fix: req:bug

This commit is contained in:
sexygoat
2026-04-20 17:51:03 +08:00
parent 11bfcadf65
commit 936a4f0656
6 changed files with 150 additions and 256 deletions

View File

@@ -42,9 +42,12 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
:value="card.iccid"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%">
<span>{{ card.iccid }}</span>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
<div>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
<ElTag v-if="card.carrier_name" size="small" type="success" style="margin-left: 5px">{{ card.carrier_name }}</ElTag>
</div>
</div>
</ElOption>
</ElSelect>

View File

@@ -374,8 +374,9 @@ export interface StandaloneIotCard {
activated_at?: string | null // 激活时间 (可选)
created_at: string // 创建时间
updated_at: string // 更新时间
series_id?: number | null // 套餐系列ID
series_name?: string // 套餐系列名称
series_id: number | null // 套餐系列ID
series_name: string // 套餐系列名称
realname_policy: string // 实名制策略
}
// ========== 单卡批量分配和回收相关 ==========

View File

@@ -10,7 +10,7 @@
</div>
<div class="card-header-right">
<ElTooltip content="开启后系统将自动轮询更新资产状态" placement="top">
<div class="polling-switch-wrapper">
<div v-if="canShowPolling" class="polling-switch-wrapper">
<span class="polling-label">自动轮询</span>
<ElSwitch
:model-value="pollingEnabled"
@@ -131,9 +131,6 @@
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">{{
cardInfo?.software_version || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeName(cardInfo?.switch_mode)
}}</ElDescriptionsItem>
@@ -211,7 +208,7 @@
size="small"
link
@click="handleEnableBindingCard(scope.row)"
v-permission="'iot_card:start'"
v-permission="'iot_info:start'"
>
启用此卡
</ElButton>
@@ -221,7 +218,7 @@
size="small"
link
@click="handleDisableBindingCard(scope.row)"
v-permission="'iot_card:stop'"
v-permission="'iot_info:stop'"
>
停用此卡
</ElButton>
@@ -367,20 +364,53 @@
class="card-operations"
style="margin-top: 16px; text-align: right"
>
<ElButton type="success" @click="handleEnableCard"> 启用此卡 </ElButton>
<ElButton type="warning" @click="handleDisableCard"> 用此卡 </ElButton>
<!--<ElButton type="danger" @click="handleManualDeactivate"> 手动停用 </ElButton>-->
<ElButton v-if="canShowRealnamePolicy" type="primary" @click="handleShowRealnamePolicy"> 实名认证策略 </ElButton>
<ElButton v-permission="'iot_info:start'" type="success" @click="handleEnableCard">
用此卡
</ElButton>
<ElButton v-permission="'iot_info:stop'" type="warning" @click="handleDisableCard">
停用此卡
</ElButton>
<!--<ElButton type="danger" v-permission="'iot_info:handler_stop'" @click="handleManualDeactivate"> 手动停用 </ElButton>-->
<!-- 手动停用暂时不开放 -->
<ElButton
v-if="canShowRealnamePolicy"
v-permission="'asset:card_realname_policy'"
type="primary"
@click="handleShowRealnamePolicy"
>
实名认证策略
</ElButton>
</div>
<!-- 设备操作按钮 -->
<div v-else class="device-operations" style="margin-top: 16px; text-align: right">
<ElButton type="info" @click="handleShowSwitchMode">设置切卡模式</ElButton>
<ElButton type="primary" @click="handleRebootDevice"> 重启设备 </ElButton>
<ElButton type="warning" @click="handleResetDevice"> 恢复出厂 </ElButton>
<ElButton type="success" @click="handleShowSwitchCard"> 切换SIM卡 </ElButton>
<ElButton type="primary" @click="handleShowSetWiFi"> 设置WiFi </ElButton>
<ElButton v-if="canShowRealnamePolicy" type="primary" @click="handleShowRealnamePolicy"> 实名认证策略 </ElButton>
<ElButton v-permission="'device_info:switch_mode'" type="info" @click="handleShowSwitchMode"
>设置切卡模式</ElButton
>
<ElButton v-permission="'device_info:reboot'" type="primary" @click="handleRebootDevice">
重启设备
</ElButton>
<ElButton v-permission="'device_info:reset'" type="warning" @click="handleResetDevice">
恢复出厂
</ElButton>
<ElButton
v-permission="'device_info:switch_card'"
type="success"
@click="handleShowSwitchCard"
>
切换SIM卡
</ElButton>
<ElButton v-permission="'device_info:set_wifi'" type="primary" @click="handleShowSetWiFi">
设置WiFi
</ElButton>
<ElButton
v-if="canShowRealnamePolicy"
v-permission="'asset_info:device_realname_policy'"
type="primary"
@click="handleShowRealnamePolicy"
>
实名认证策略
</ElButton>
</div>
</ElCard>
</template>
@@ -499,11 +529,21 @@
const { hasAuth } = useAuth()
const realnamePolicyPermission = computed(() => {
return props.cardInfo?.asset_type === 'card' ? 'asset:card_realname_policy' : 'asset:device_realname_policy'
return props.cardInfo?.asset_type === 'card'
? 'asset:card_realname_policy'
: 'asset:device_realname_policy'
})
const canShowRealnamePolicy = computed(() => hasAuth(realnamePolicyPermission.value))
const pollingPermission = computed(() => {
return props.cardInfo?.asset_type === 'card'
? 'iot_info:auto_polling'
: 'device_info:auto_polling'
})
const canShowPolling = computed(() => hasAuth(pollingPermission.value))
// Emits
interface Emits {
(e: 'update:pollingEnabled', value: boolean): void

View File

@@ -87,7 +87,8 @@
<!-- 对话框组件 -->
<SwitchCardDialog
v-model="switchCardDialogVisible"
:cards="cardInfo?.cards"
:device-info="cardInfo?.virtual_no || ''"
:cards="cardInfo?.cards || []"
@confirm="handleConfirmSwitchCard"
/>
<SwitchModeDialog
@@ -140,13 +141,13 @@
// 引入对话框组件
import {
SwitchCardDialog,
SwitchModeDialog,
WiFiConfigDialog,
PackageRechargeDialog,
DailyRecordsDialog,
OrderHistoryDialog
} from './components/dialogs'
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
// 引入 composables

View File

@@ -335,19 +335,24 @@
ref="bindCardFormRef"
:model="bindCardForm"
:rules="bindCardRules"
label-width="100px"
label-width="80"
>
<ElRow :gutter="20">
<ElCol :span="10">
<ElFormItem label="IoT卡" prop="iot_card_id">
<ElSelect
v-model="bindCardForm.iot_card_id"
placeholder="请选择或搜索IoT卡支持ICCID搜索"
:placeholder="
iotCardList.length === 0 && !iotCardSearchLoading
? '当前没有未绑定设备的卡'
: '请选择或搜索IoT卡支持ICCID搜索'
"
filterable
remote
reserve-keyword
:remote-method="searchIotCards"
:loading="iotCardSearchLoading"
:disabled="iotCardList.length === 0 && !iotCardSearchLoading"
clearable
style="width: 100%"
>
@@ -376,7 +381,7 @@
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElCol :span="3">
<ElButton
type="primary"
@click="handleConfirmBindCard"
@@ -438,76 +443,14 @@
</ElDialog>
<!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
<div v-if="loadingDeviceCards" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div>
<template v-else>
<ElAlert
v-if="deviceBindingCards.length === 0"
title="该设备暂无绑定的SIM卡"
type="warning"
:closable="false"
style="margin-bottom: 20px"
>
<template #default>
<div>当前设备没有绑定任何SIM卡,无法进行切换操作</div>
<div>请先为设备绑定SIM卡后再进行切换</div>
</template>
</ElAlert>
<ElForm
v-if="deviceBindingCards.length > 0"
ref="switchCardFormRef"
:model="switchCardForm"
:rules="switchCardRules"
label-width="120px"
>
<ElFormItem label="设备号">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingImei }}</span>
</ElFormItem>
<ElFormItem label="目标ICCID" prop="target_iccid">
<ElSelect
v-model="switchCardForm.target_iccid"
placeholder="请选择要切换到的目标ICCID"
style="width: 100%"
clearable
>
<ElOption
v-for="card in deviceBindingCards"
:key="card.iccid"
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}${card.is_current ? ' (当前)' : ''}`"
:value="card.iccid"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ card.iccid }}</span>
<ElTag size="small" style="margin-left: 10px"
>插槽{{ card.slot_position }}</ElTag
>
</div>
</ElOption>
</ElSelect>
<div style="margin-top: 8px; font-size: 12px; color: #909399">
当前设备共绑定 {{ deviceBindingCards.length }} 张SIM卡
</div>
</ElFormItem>
</ElForm>
</template>
<template #footer>
<ElButton @click="switchCardDialogVisible = false">取消</ElButton>
<ElButton
v-if="deviceBindingCards.length > 0"
type="primary"
@click="handleConfirmSwitchCard"
:loading="switchCardLoading"
>
确认切换
</ElButton>
</template>
</ElDialog>
<SwitchCardDialog
v-model="switchCardDialogVisible"
:device-info="currentOperatingImei"
:cards="deviceBindingCards"
:loading="loadingDeviceCards"
:confirm-loading="switchCardLoading"
@confirm="handleConfirmSwitchCard"
/>
<!-- 设置WiFi对话框 -->
<ElDialog v-model="setWiFiDialogVisible" title="设置WiFi" width="500px">
@@ -566,7 +509,9 @@
label-width="120px"
>
<ElFormItem label="设备标识">
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDeviceIdentifier }}</span>
<span style="font-weight: bold; color: #409eff">{{
currentOperatingDeviceIdentifier
}}</span>
</ElFormItem>
<ElFormItem label="当前模式">
<ElTag :type="currentDeviceSwitchMode === 0 ? 'success' : 'warning'">
@@ -621,6 +566,7 @@
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
import type {
Device,
DeviceStatus,
@@ -708,6 +654,7 @@
const bindCardFormRef = ref<FormInstance>()
const iotCardList = ref<any[]>([])
const iotCardSearchLoading = ref(false)
const iotCardHasSearched = ref(false) // 标记是否已执行过搜索
const bindCardForm = reactive({
iot_card_id: undefined as number | undefined,
slot_position: 1
@@ -724,13 +671,6 @@
const switchCardDialogVisible = ref(false)
const switchCardLoading = ref(false)
const switchCardFormRef = ref<FormInstance>()
const switchCardForm = reactive({
target_iccid: ''
})
const switchCardRules = reactive<FormRules>({
target_iccid: [{ required: true, message: '请选择目标ICCID', trigger: 'change' }]
})
const deviceBindingCards = ref<any[]>([]) // 设备绑定的卡列表
const loadingDeviceCards = ref(false) // 加载设备绑定卡列表的状态
@@ -868,7 +808,7 @@
{ label: '已绑定卡数', prop: 'bound_card_count' },
{ label: '状态', prop: 'status' },
{ label: '在线状态', prop: 'online_status' },
{ label: '固件版本', prop: 'software_version' },
{ label: '切卡模式', prop: 'switch_mode' },
{ label: '最后在线时间', prop: 'last_online_time' },
{ label: '最后同步时间', prop: 'last_gateway_sync_at' },
@@ -917,8 +857,8 @@
// 重置绑定卡表单
bindCardForm.iot_card_id = undefined
bindCardForm.slot_position = 1
// 加载设备卡列表和默认IoT卡列表
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
// 加载设备卡列表
await loadDeviceCards(device.virtual_no)
}
// 加载设备绑定的卡列表
@@ -927,15 +867,9 @@
try {
const res = await DeviceService.getDeviceCards(virtualNo)
if (res.code === 0 && res.data) {
deviceCards.value = res.data.bindings || []
console.log('设备绑定的卡列表:', deviceCards.value)
// 🔧 临时调试强制第一张卡显示为当前卡方便测试UI效果
// 测试完成后请删除此代码
if (deviceCards.value.length > 0) {
deviceCards.value[0].is_current = true
console.log('🧪 测试模式:已将第一张卡标记为当前卡')
}
deviceCards.value = (res.data.bindings || []).sort(
(a, b) => a.slot_position - b.slot_position
)
}
} catch (error) {
console.error('获取设备绑定的卡列表失败:', error)
@@ -950,7 +884,8 @@
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20
page_size: 20,
is_standalone: true
})
if (res.code === 0 && res.data?.items) {
iotCardList.value = res.data.items
@@ -969,11 +904,13 @@
return
}
iotCardSearchLoading.value = true
iotCardHasSearched.value = true
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20,
iccid: query
iccid: query,
is_standalone: true
})
if (res.code === 0 && res.data?.items) {
iotCardList.value = res.data.items
@@ -1182,13 +1119,6 @@
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
prop: 'software_version',
label: '固件版本',
width: 120,
showOverflowTooltip: true,
formatter: (row: Device) => row.software_version || '-'
},
{
prop: 'switch_mode',
label: '切卡模式',
@@ -1201,6 +1131,20 @@
return modeMap[row.switch_mode || ''] || '-'
}
},
{
prop: 'realname_policy',
label: '实名认证策略',
width: 150,
formatter: (row: Device) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
return policyMap[policy] || (policy ? policy : '未知')
}
},
{
prop: 'last_online_time',
label: '最后在线时间',
@@ -1229,13 +1173,17 @@
}
])
let isFirstActivation = true
onMounted(() => {
getTableData()
})
// 当页面被 keep-alive 激活时自动刷新数据
onActivated(() => {
getTableData()
if (!isFirstActivation) {
getTableData()
}
isFirstActivation = false
})
// 获取设备列表
@@ -1758,7 +1706,6 @@
// 显示切换SIM卡对话框
const showSwitchCardDialog = async (imei: string) => {
currentOperatingImei.value = imei
switchCardForm.target_iccid = ''
deviceBindingCards.value = []
switchCardDialogVisible.value = true
@@ -1775,14 +1722,6 @@
const res = await DeviceService.getDeviceCards(device.virtual_no)
if (res.code === 0 && res.data) {
deviceBindingCards.value = res.data.bindings || []
// 🔧 临时调试强制第一张卡显示为当前卡方便测试UI效果
// 测试完成后请删除此代码
if (deviceBindingCards.value.length > 0) {
deviceBindingCards.value[0].is_current = true
console.log('🧪 切换SIM卡测试模式已将第一张卡标记为当前卡')
}
if (deviceBindingCards.value.length === 0) {
ElMessage.warning('该设备暂无绑定的SIM卡')
}
@@ -1795,29 +1734,20 @@
}
// 确认切换SIM卡
const handleConfirmSwitchCard = async () => {
if (!switchCardFormRef.value) return
await switchCardFormRef.value.validate(async (valid) => {
if (valid) {
switchCardLoading.value = true
try {
const res = await DeviceService.switchCard(currentOperatingImei.value, {
target_iccid: switchCardForm.target_iccid
})
if (res.code === 0) {
ElMessage.success('切换SIM卡指令已发送')
switchCardDialogVisible.value = false
} else {
ElMessage.error(res.msg || '切换失败')
}
} catch (error: any) {
console.error('切换SIM卡失败:', error)
} finally {
switchCardLoading.value = false
}
const handleConfirmSwitchCard = async (data: { target_iccid: string }) => {
try {
const res = await DeviceService.switchCard(currentOperatingImei.value, {
target_iccid: data.target_iccid
})
if (res.code === 0) {
ElMessage.success('切换SIM卡指令已发送')
switchCardDialogVisible.value = false
} else {
ElMessage.error(res.msg || '切换失败')
}
})
} catch (error: any) {
console.error('切换SIM卡失败:', error)
}
}
// 显示设置WiFi对话框

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
label-width="90"
label-width="120"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -927,7 +927,7 @@
]
},
{
label: '独立卡',
label: '是否绑定设备',
prop: 'is_standalone',
type: 'select',
config: {
@@ -955,6 +955,7 @@
{ label: '激活状态', prop: 'activation_status' },
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '实名认证策略', prop: 'realname_policy' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '创建时间', prop: 'created_at' }
]
@@ -1046,10 +1047,6 @@
return typeMap[status] || 'info'
}
const formatCardPrice = (price: number) => {
return `¥${(price / 100).toFixed(2)}`
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
@@ -1151,6 +1148,21 @@
return h(ElTag, { type }, () => text)
}
},
{
prop: 'realname_policy',
label: '实名认证策略',
width: 150,
formatter: (row: StandaloneIotCard) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
const text = policyMap[policy] || (policy ? policy : '未知')
return text
}
},
{
prop: 'data_usage_mb',
label: '累计流量(MB)',
@@ -1711,99 +1723,6 @@
})
}
// 查询流量使用
const showFlowUsageDialog = async (iccid: string) => {
flowUsageDialogVisible.value = true
flowUsageLoading.value = true
flowUsageData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的流量信息
flowUsageData.value = {
totalFlow: res.data.package_total_mb || 0,
usedFlow: res.data.package_used_mb || 0,
remainFlow: res.data.package_remain_mb || 0,
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
flowUsageDialogVisible.value = false
}
} catch (error: any) {
console.error('查询流量使用失败:', error)
flowUsageDialogVisible.value = false
} finally {
flowUsageLoading.value = false
}
}
// 查询实名认证状态
const showRealnameStatusDialog = async (iccid: string) => {
realnameStatusDialogVisible.value = true
realnameStatusLoading.value = true
realnameStatusData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的实名状态real_name_status: 0未实名 1已实名
let statusText = '未知'
switch (res.data.real_name_status) {
case 0:
statusText = '未实名'
break
case 1:
statusText = '已实名'
break
}
realnameStatusData.value = {
status: statusText,
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
realnameStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询实名状态失败:', error)
realnameStatusDialogVisible.value = false
} finally {
realnameStatusLoading.value = false
}
}
// 查询卡实时状态
const showCardStatusDialog = async (iccid: string) => {
cardStatusDialogVisible.value = true
cardStatusLoading.value = true
cardStatusData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的网络状态
cardStatusData.value = {
iccid: iccid,
cardStatus: res.data.network_status === 1 ? '正常' : '停机',
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
cardStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询卡状态失败:', error)
cardStatusDialogVisible.value = false
} finally {
cardStatusLoading.value = false
}
}
// 启用此卡(复机)
const handleStartCard = (iccid: string) => {
ElMessageBox.confirm('确定要启用该卡片吗?', '确认启用', {