diff --git a/src/assets/styles/el-ui.scss b/src/assets/styles/el-ui.scss index 85bb447..aec4ef9 100644 --- a/src/assets/styles/el-ui.scss +++ b/src/assets/styles/el-ui.scss @@ -280,6 +280,11 @@ transform: scale(0.6) !important; } } + + .el-checkbox__input.is-disabled .el-checkbox__inner { + background-color: var(--el-color-primary) !important; + border-color: var(--el-color-primary) !important; + } } .el-notification .el-notification__icon { diff --git a/src/template/IoT卡导入模板.xlsx b/src/template/IoT卡导入模板.xlsx new file mode 100644 index 0000000..8529fd0 Binary files /dev/null and b/src/template/IoT卡导入模板.xlsx differ diff --git a/src/template/设备导入模板.xlsx b/src/template/设备导入模板.xlsx new file mode 100644 index 0000000..48b209e Binary files /dev/null and b/src/template/设备导入模板.xlsx differ diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue index 2f0202e..dc9b1e0 100644 --- a/src/views/account-management/account/index.vue +++ b/src/views/account-management/account/index.vue @@ -402,7 +402,7 @@ ] // 显示对话框 - const showDialog = (type: string, row?: any) => { + const showDialog = async (type: string, row?: any) => { dialogVisible.value = true dialogType.value = type @@ -411,6 +411,10 @@ formRef.value.resetFields() } + if (type === 'add') { + await loadAllRoles() + } + if (type === 'edit' && row) { formData.id = row.id formData.username = row.username @@ -590,7 +594,8 @@ try { const params: any = { page: 1, - page_size: keyword ? 100 : 20 // 搜索时加载更多,默认20条 + page_size: keyword ? 100 : 20, // 搜索时加载更多,默认20条 + role_type: 1 } if (keyword) { params.role_name = keyword @@ -604,48 +609,14 @@ } } - // 计算属性:新建账号时可选的角色(根据账号类型过滤) + // 计算属性:新建账号时可选的角色 const availableRolesForCreate = computed(() => { - let roles = allRoles.value - - // 根据账号类型过滤角色 - if (formData.user_type === 1) { - // 超级管理员:不能分配角色 - return [] - } else if (formData.user_type === 2) { - // 平台用户:只显示平台角色 - roles = roles.filter((role) => role.role_type === 1) - } else if (formData.user_type === 3) { - // 代理账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } else if (formData.user_type === 4) { - // 企业账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } - - return roles + return allRoles.value }) - // 计算属性:过滤后的可分配角色(根据账号类型过滤) + // 计算属性:过滤后的可分配角色 const filteredAvailableRoles = computed(() => { - let roles = allRoles.value - - // 根据账号类型过滤角色 - if (currentAccountType.value === 1) { - // 超级管理员:不能分配任何角色 - return [] - } else if (currentAccountType.value === 3) { - // 代理账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } else if (currentAccountType.value === 4) { - // 企业账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } else if (currentAccountType.value === 2) { - // 平台用户:只显示平台角色 - roles = roles.filter((role) => role.role_type === 1) - } - - return roles + return allRoles.value }) // 搜索防抖定时器 diff --git a/src/views/asset-management/asset-information/components/dialogs/DailyRecordsDialog.vue b/src/views/asset-management/asset-information/components/dialogs/DailyRecordsDialog.vue index 8345c4c..034c747 100644 --- a/src/views/asset-management/asset-information/components/dialogs/DailyRecordsDialog.vue +++ b/src/views/asset-management/asset-information/components/dialogs/DailyRecordsDialog.vue @@ -6,9 +6,6 @@ {{ data.package_name }} - - {{ data.package_usage_id }} - {{ formatDataSize(data.total_usage_mb) }} diff --git a/src/views/asset-management/asset-information/composables/useAssetInfo.ts b/src/views/asset-management/asset-information/composables/useAssetInfo.ts index fe7facc..7a8fae7 100644 --- a/src/views/asset-management/asset-information/composables/useAssetInfo.ts +++ b/src/views/asset-management/asset-information/composables/useAssetInfo.ts @@ -171,20 +171,25 @@ export function useAssetInfo() { const loadCurrentPackage = async (identifier: string) => { try { currentPackageLoading.value = true - // 清空之前的错误信息 currentPackageErrorMsg.value = '' - const response = await AssetService.getCurrentPackage(identifier) - if (response.code === 0) { - if (response.data) { - const pkg = response.data + const [currentPkgResponse, packageListResponse] = await Promise.all([ + AssetService.getCurrentPackage(identifier), + AssetService.getAssetPackages(identifier, { page: 1, page_size: 50 }) + ]) + + const activePackage = packageListResponse.data?.items?.find((p: any) => p.status === 1) + const activePaidAmount = activePackage?.paid_amount || 0 + + if (currentPkgResponse.code === 0) { + if (currentPkgResponse.data) { + const pkg = currentPkgResponse.data - // 转换为组件期望的格式 currentPackage.value = { id: pkg.package_usage_id || 0, package_id: pkg.package_id || 0, package_name: pkg.package_name || '', - package_price: 0, // API 没有返回价格 + package_price: activePaidAmount, package_total_data: pkg.data_limit_mb || 0, real_data_used: pkg.data_usage_mb || 0, real_data_remaining: (pkg.data_limit_mb || 0) - (pkg.data_usage_mb || 0), @@ -196,10 +201,10 @@ export function useAssetInfo() { expire_time: pkg.expires_at || '', status: pkg.status || 0, status_name: pkg.status_name, - duration_days: undefined, // API 没有返回时长 - virtual_ratio: pkg.virtual_ratio, // 虚流量比例 - package_type: pkg.package_type, // 套餐类型 - enable_virtual_data: pkg.enable_virtual_data, // 是否启用虚流量 + duration_days: undefined, + virtual_ratio: pkg.virtual_ratio, + package_type: pkg.package_type, + enable_virtual_data: pkg.enable_virtual_data, data_limit_mb: pkg.data_limit_mb, virtual_limit_mb: pkg.virtual_limit_mb, virtual_remain_mb: pkg.virtual_remain_mb, diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue index 517398e..ec46db4 100644 --- a/src/views/asset-management/device-list/index.vue +++ b/src/views/asset-management/device-list/index.vue @@ -338,34 +338,47 @@ label-width="80" > - - + + + + + + + + + + + - + ({ - iot_card_id: [{ required: true, message: '请选择IoT卡', trigger: 'change' }], + search_type: [{ required: true, message: '请选择搜索类型', trigger: 'change' }], + iot_card_id: [{ required: true, message: '请选择ICCID', trigger: 'change' }], slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }] }) @@ -855,10 +871,14 @@ deviceCards.value = [] deviceCardsDialogVisible.value = true // 重置绑定卡表单 + bindCardForm.search_type = 'iccid' bindCardForm.iot_card_id = undefined bindCardForm.slot_position = 1 - // 只加载设备卡列表 - await loadDeviceCards(device.virtual_no) + // 加载未绑定设备的卡列表 + await Promise.all([ + loadDeviceCards(device.virtual_no), + loadDefaultIotCards() + ]) } // 加载设备绑定的卡列表 @@ -897,7 +917,7 @@ } } - // 搜索IoT卡(根据ICCID) + // 搜索IoT卡(根据ICCID或运营商名称) const searchIotCards = async (query: string) => { if (!query) { await loadDefaultIotCards() @@ -906,12 +926,17 @@ iotCardSearchLoading.value = true iotCardHasSearched.value = true try { - const res = await CardService.getStandaloneIotCards({ + const params: any = { page: 1, page_size: 20, - iccid: query, is_standalone: true - }) + } + if (bindCardForm.search_type === 'iccid') { + params.iccid = query + } else if (bindCardForm.search_type === 'carrier_name') { + params.carrier_name = query + } + const res = await CardService.getStandaloneIotCards(params) if (res.code === 0 && res.data?.items) { iotCardList.value = res.data.items } diff --git a/src/views/asset-management/iot-card-management/index.vue b/src/views/asset-management/iot-card-management/index.vue index e1e1f6f..3706dcc 100644 --- a/src/views/asset-management/iot-card-management/index.vue +++ b/src/views/asset-management/iot-card-management/index.vue @@ -562,7 +562,7 @@ import { h } from 'vue' import { useRouter } from 'vue-router' import { RoutesAlias } from '@/router/routesAlias' - import { CardService, ShopService, PackageSeriesService, AssetService } from '@/api/modules' + import { CardService, ShopService, PackageSeriesService, AssetService, CarrierService } from '@/api/modules' import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus' import { Loading } from '@element-plus/icons-vue' import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue' @@ -627,6 +627,38 @@ failed_items: null }) + // 运营商搜索相关 + const carrierLoading = ref(false) + const searchCarrierOptions = ref([]) + + // 加载搜索栏运营商选项(默认加载10条) + const loadSearchCarrierOptions = async (carrierName?: string) => { + try { + const params: any = { + page: 1, + page_size: 10 + } + if (carrierName) { + params.carrier_name = carrierName + } + const res = await CarrierService.getCarriers(params) + if (res.code === 0) { + searchCarrierOptions.value = res.data.items + } + } catch (error) { + console.error('加载运营商选项失败:', error) + } + } + + // 搜索运营商(用于搜索栏) + const handleSearchCarrier = (query: string) => { + if (query) { + loadSearchCarrierOptions(query) + } else { + loadSearchCarrierOptions() + } + } + // 卡详情弹窗相关 const cardDetailDialogVisible = ref(false) const cardDetailLoading = ref(false) @@ -730,8 +762,8 @@ // 搜索表单初始值 const initialSearchState: { status: undefined | number - carrier_id: undefined | number iccid: string + carrier_name: string msisdn: string virtual_no: string device_virtual_no: string @@ -740,8 +772,8 @@ [key: string]: any } = { status: undefined, - carrier_id: undefined, iccid: '', + carrier_name: '', msisdn: '', virtual_no: '', device_virtual_no: '', @@ -863,20 +895,6 @@ { label: '已停用', value: 4 } ] }, - { - label: '运营商', - prop: 'carrier_id', - type: 'select', - config: { - clearable: true, - placeholder: '全部' - }, - options: () => [ - { label: '中国移动', value: 1 }, - { label: '中国联通', value: 2 }, - { label: '中国电信', value: 3 } - ] - }, { label: 'ICCID', prop: 'iccid', @@ -886,6 +904,24 @@ placeholder: '请输入ICCID' } }, + { + label: '运营商名称', + prop: 'carrier_name', + type: 'select', + config: { + clearable: true, + filterable: true, + remote: true, + remoteMethod: handleSearchCarrier, + loading: carrierLoading.value, + placeholder: '请选择或搜索运营商名称' + }, + options: () => + searchCarrierOptions.value.map((c) => ({ + label: c.carrier_name, + value: c.carrier_name + })) + }, { label: '卡接入号', prop: 'msisdn', @@ -1180,20 +1216,22 @@ const getActions = (row: StandaloneIotCard) => { const actions: any[] = [] - if (hasAuth('iot_card:start_card')) { - actions.push({ - label: '启用此卡', - handler: () => handleCardOperation('start-card', row.iccid), - type: 'primary' - }) - } - - if (hasAuth('iot_card:stop_card')) { - actions.push({ - label: '停用此卡', - handler: () => handleCardOperation('stop-card', row.iccid), - type: 'primary' - }) + if (row.network_status === 1) { + if (hasAuth('iot_card:stop_card')) { + actions.push({ + label: '停用此卡', + handler: () => handleCardOperation('stop-card', row.iccid), + type: 'primary' + }) + } + } else { + if (hasAuth('iot_card:start_card')) { + actions.push({ + label: '启用此卡', + handler: () => handleCardOperation('start-card', row.iccid), + type: 'primary' + }) + } } if (hasAuth('iot_card:realname_policy')) { @@ -1216,6 +1254,7 @@ } onMounted(() => { + loadSearchCarrierOptions() getTableData() }) @@ -1228,7 +1267,6 @@ page_size: pagination.pageSize, ...formFilters } - // 清理空值 Object.keys(params).forEach((key) => { if (params[key] === '' || params[key] === undefined) { delete params[key] diff --git a/src/views/asset-management/task-management/device-task/index.vue b/src/views/asset-management/task-management/device-task/index.vue index fcd315c..337d9ad 100644 --- a/src/views/asset-management/task-management/device-task/index.vue +++ b/src/views/asset-management/task-management/device-task/index.vue @@ -61,7 +61,7 @@

2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条

3. 列格式请设置为文本格式,避免长数字被转为科学计数法

4. 重要:列顺序固定,不可调整。系统按位置读取,不识别列名

-

5. 必填列:虚拟号(第1列)

+

5. 必填列:虚拟号(第1列)

6. 可选列:设备名称、设备型号、设备类型、IMEI、制造商、最大SIM槽数(默认4,有效范围1-4)、卡1~卡4 @@ -443,77 +443,16 @@ // 下载模板 const downloadTemplate = async () => { try { - // 动态导入 xlsx 库 - const XLSX = await import('xlsx') - - // 创建示例数据(按固定列顺序) - const templateData = [ - { - 虚拟号: '862639070731999', - 设备名称: '智能水表01', - 设备型号: 'WM-2000', - 设备类型: '智能水表', - IMEI: '860123456789012', - 制造商: '华为', - 最大SIM槽数: 4, - '卡1 ICCID': '89860123456789012345', - '卡2 ICCID': '', - '卡3 ICCID': '', - '卡4 ICCID': '' - }, - { - 虚拟号: '862639070750932', - 设备名称: 'GPS定位器01', - 设备型号: 'GPS-3000', - 设备类型: '定位设备', - IMEI: '860123456789013', - 制造商: '小米', - 最大SIM槽数: 2, - '卡1 ICCID': '89860123456789012346', - '卡2 ICCID': '89860123456789012347', - '卡3 ICCID': '', - '卡4 ICCID': '' - } - ] - - // 创建工作簿 - const wb = XLSX.utils.book_new() - const ws = XLSX.utils.json_to_sheet(templateData) - - // 设置列宽 - ws['!cols'] = [ - { wch: 20 }, // 虚拟号 - { wch: 15 }, // 设备名称 - { wch: 15 }, // 设备型号 - { wch: 15 }, // 设备类型 - { wch: 18 }, // IMEI - { wch: 12 }, // 制造商 - { wch: 15 }, // 最大SIM槽数 - { wch: 25 }, // 卡1 ICCID - { wch: 25 }, // 卡2 ICCID - { wch: 25 }, // 卡3 ICCID - { wch: 25 } // 卡4 ICCID - ] - - // 将所有单元格设置为文本格式 - const range = XLSX.utils.decode_range(ws['!ref'] || 'A1') - for (let R = range.s.r; R <= range.e.r; ++R) { - for (let C = range.s.c; C <= range.e.c; ++C) { - const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }) - if (!ws[cellAddress]) continue - ws[cellAddress].t = 's' // 设置为字符串类型 - } - } - - // 添加工作表到工作簿 - XLSX.utils.book_append_sheet(wb, ws, '设备导入模板') - - // 导出文件 - XLSX.writeFile(wb, '设备导入模板.xlsx') - + const link = document.createElement('a') + link.href = new URL('@/template/设备导入模板.xlsx', import.meta.url).href + link.download = '设备导入模板.xlsx' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) ElMessage.success('设备导入模板下载成功') } catch (error) { console.error('下载模板失败:', error) + ElMessage.error('下载模板失败') } } diff --git a/src/views/asset-management/task-management/iot-card-task/index.vue b/src/views/asset-management/task-management/iot-card-task/index.vue index b364654..e7f90a9 100644 --- a/src/views/asset-management/task-management/iot-card-task/index.vue +++ b/src/views/asset-management/task-management/iot-card-task/index.vue @@ -105,10 +105,8 @@

1. 请先下载 Excel 模板文件,按照模板格式填写IoT卡信息

2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条

3. 列格式请设置为文本格式,避免长数字被转为科学计数法

-

4. 必填字段:ICCID、MSISDN(手机号)、virtual_no(虚拟号)

-

5. Excel 文件必须包含 virtual_no 列,否则整批导入将失败

-

6. virtual_no 为空将导入失败

-

7. 必须选择运营商

+

4. 必填字段:ICCID、MSISDN

+

5. 必须选择运营商

@@ -642,58 +640,16 @@ // 下载模板 const downloadTemplate = async () => { try { - // 动态导入 xlsx 库 - const XLSX = await import('xlsx') - - // 创建示例数据(按固定列顺序:ICCID、MSISDN、虚拟号) - const templateData = [ - { - ICCID: '89860123456789012345', - 'MSISDN(接入号)': '13800138000', - 虚拟号: 'V001' - }, - { - ICCID: '89860123456789012346', - 'MSISDN(接入号)': '13800138001', - 虚拟号: 'V002' - }, - { - ICCID: '89860123456789012347', - 'MSISDN(接入号)': '13800138002', - 虚拟号: 'V003' - } - ] - - // 创建工作簿 - const wb = XLSX.utils.book_new() - const ws = XLSX.utils.json_to_sheet(templateData) - - // 设置列宽 - ws['!cols'] = [ - { wch: 25 }, // ICCID - { wch: 20 }, // MSISDN(接入号) - { wch: 15 } // 虚拟号 - ] - - // 将所有单元格设置为文本格式,防止科学计数法 - const range = XLSX.utils.decode_range(ws['!ref'] || 'A1') - for (let R = range.s.r; R <= range.e.r; ++R) { - for (let C = range.s.c; C <= range.e.c; ++C) { - const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }) - if (!ws[cellAddress]) continue - ws[cellAddress].t = 's' // 设置为字符串类型 - } - } - - // 添加工作表到工作簿 - XLSX.utils.book_append_sheet(wb, ws, 'IoT卡导入模板') - - // 导出文件 - XLSX.writeFile(wb, 'IoT卡导入模板.xlsx') - + const link = document.createElement('a') + link.href = new URL('@/template/IoT卡导入模板.xlsx', import.meta.url).href + link.download = 'IoT卡导入模板.xlsx' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) ElMessage.success('IoT卡导入模板下载成功') } catch (error) { console.error('下载模板失败:', error) + ElMessage.error('下载模板失败') } } diff --git a/src/views/common/account-list.vue b/src/views/common/account-list.vue index 628fad2..d370510 100644 --- a/src/views/common/account-list.vue +++ b/src/views/common/account-list.vue @@ -582,7 +582,8 @@ try { const params: any = { page: 1, - page_size: 20 + page_size: 20, + role_type: 1 } if (keyword) { params.role_name = keyword @@ -607,23 +608,9 @@ }, 300) } - // 计算属性:过滤后的可分配角色(根据账号类型过滤) + // 计算属性:过滤后的可分配角色 const filteredAvailableRoles = computed(() => { - let roles = allRoles.value - - // 根据账号类型过滤角色 - if (currentAccountType.value === 3) { - // 代理账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } else if (currentAccountType.value === 2) { - // 平台用户:只显示平台角色 - roles = roles.filter((role) => role.role_type === 1) - } else if (currentAccountType.value === 4) { - // 企业账号:只显示客户角色 - roles = roles.filter((role) => role.role_type === 2) - } - - return roles + return allRoles.value }) // 计算属性:过滤后的已分配角色 diff --git a/src/views/finance/agent-recharge/detail.vue b/src/views/finance/agent-recharge/detail.vue index f932d50..c4d7018 100644 --- a/src/views/finance/agent-recharge/detail.vue +++ b/src/views/finance/agent-recharge/detail.vue @@ -94,9 +94,7 @@ title: '订单信息', fields: [ { label: '充值单号', prop: 'recharge_no' }, - { label: '充值记录ID', prop: 'id' }, { label: '店铺名称', prop: 'shop_name' }, - { label: '店铺ID', prop: 'shop_id' }, { label: '充值金额', formatter: (_, data) => formatCurrency(data.amount) @@ -122,15 +120,6 @@ prop: 'payment_channel', formatter: (value) => value || '-' }, - { - label: '支付配置ID', - prop: 'payment_config_id', - formatter: (value) => value || '-' - }, - { - label: '代理钱包ID', - prop: 'agent_wallet_id' - }, { label: '第三方支付流水号', prop: 'payment_transaction_id', diff --git a/src/views/finance/refund/detail.vue b/src/views/finance/refund/detail.vue index af0cda4..9858a0b 100644 --- a/src/views/finance/refund/detail.vue +++ b/src/views/finance/refund/detail.vue @@ -14,12 +14,6 @@ {{ getStatusText(refund.status) }} - {{ refund.order_id }} - {{ refund.shop_id }} - - - {{ refund.package_usage_id || '-' }} - {{ formatCurrency(refund.requested_refund_amount) }} @@ -42,34 +36,29 @@ - + {{ refund.refund_reason || '-' }} - + {{ refund.reject_reason || '-' }} - - {{ refund.remark || '-' }} + + {{ refund.processed_at ? formatDateTime(refund.processed_at) : '-' }} - {{ refund.creator }} - - {{ refund.processor_id || '-' }} + + {{ refund.remark || '-' }} {{ formatDateTime(refund.created_at) }} - - {{ refund.processed_at ? formatDateTime(refund.processed_at) : '-' }} - {{ formatDateTime(refund.updated_at) }} - {{ refund.updater }}
diff --git a/src/views/finance/refund/index.vue b/src/views/finance/refund/index.vue index a634f48..a8eb03d 100644 --- a/src/views/finance/refund/index.vue +++ b/src/views/finance/refund/index.vue @@ -63,11 +63,12 @@ placeholder="请输入订单号搜索" style="width: 100%" @focus="handleOrderSelectFocus" + @change="handleOrderSelectChange" > @@ -75,21 +76,18 @@ - - + @@ -434,8 +432,7 @@ const createRules = reactive({ order_id: [{ required: true, message: '请输入订单ID', trigger: 'blur' }], - requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }], - actual_received_amount: [{ required: true, message: '请输入实收金额', trigger: 'blur' }] + requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }] }) const approveRules = reactive({}) @@ -485,6 +482,15 @@ const refundList = ref([]) + // 选中的订单实收金额 + const selectedOrderActualPaid = computed(() => { + if (!createForm.order_id) return 0 + const selectedOrder = orderSearchOptions.value.find((o) => o.id === createForm.order_id) + if (!selectedOrder?.actual_paid_amount) return 0 + const amount = selectedOrder.actual_paid_amount / 100 + return isNaN(amount) ? 0 : amount + }) + // 格式化货币 - 将分转换为元 const formatCurrency = (amount: number): string => { return `¥${(amount / 100).toFixed(2)}` @@ -684,6 +690,18 @@ } } + // 订单选择变化 - 自动填充实收金额 + const handleOrderSelectChange = (orderId: number | null) => { + if (!orderId) { + createForm.actual_received_amount = 0 + return + } + const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId) + if (selectedOrder) { + createForm.actual_received_amount = selectedOrder.actual_paid_amount / 100 + } + } + // 获取退款申请列表 const getTableData = async () => { loading.value = true