modify-bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m58s

This commit is contained in:
sexygoat
2026-03-28 18:31:43 +08:00
parent d6c23c8bd1
commit dbe6070207
22 changed files with 922 additions and 260 deletions

View File

@@ -16,7 +16,10 @@
"Bash(npm run dev:*)", "Bash(npm run dev:*)",
"Bash(timeout:*)", "Bash(timeout:*)",
"Read(//d/**)", "Read(//d/**)",
"Bash(findstr:*)" "Bash(findstr:*)",
"Bash(npm run:*)",
"Bash(find src/views -name *.vue -type f -exec grep -l \"device.*detail\\\\|设备详情\" {})",
"Bash(2)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -11,6 +11,8 @@ import type {
AssetRealtimeStatusResponse, AssetRealtimeStatusResponse,
AssetRefreshResponse, AssetRefreshResponse,
AssetPackageUsageRecord, AssetPackageUsageRecord,
AssetPackageListResponse,
AssetPackageParams,
AssetCurrentPackageResponse, AssetCurrentPackageResponse,
DeviceStopResponse, DeviceStopResponse,
DeviceStartResponse, DeviceStartResponse,
@@ -65,17 +67,20 @@ export class AssetService extends BaseService {
} }
/** /**
* 查询该资产所有套餐记录,含虚流量换算字段 * 查询该资产所有套餐记录,含虚流量换算字段(分页)
* GET /api/admin/assets/:asset_type/:id/packages * GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50
* @param assetType 资产类型 (card 或 device) * @param assetType 资产类型 (card 或 device)
* @param id 资产ID * @param id 资产ID
* @param params 查询参数(可选分页参数)
*/ */
static getAssetPackages( static getAssetPackages(
assetType: AssetType, assetType: AssetType,
id: number id: number,
): Promise<BaseResponse<AssetPackageUsageRecord[]>> { params?: AssetPackageParams
return this.get<BaseResponse<AssetPackageUsageRecord[]>>( ): Promise<BaseResponse<AssetPackageListResponse>> {
`/api/admin/assets/${assetType}/${id}/packages` return this.get<BaseResponse<AssetPackageListResponse>>(
`/api/admin/assets/${assetType}/${id}/packages`,
params
) )
} }

View File

@@ -22,7 +22,8 @@ import type {
MyCommissionStatsQueryParams, MyCommissionStatsQueryParams,
MyCommissionStatsResponse, MyCommissionStatsResponse,
MyDailyCommissionStatsQueryParams, MyDailyCommissionStatsQueryParams,
DailyCommissionStatsItem DailyCommissionStatsItem,
ResolveCommissionParams
} from '@/types/api/commission' } from '@/types/api/commission'
export class CommissionService extends BaseService { export class CommissionService extends BaseService {
@@ -202,4 +203,17 @@ export class CommissionService extends BaseService {
params params
) )
} }
// ==================== 佣金记录修正 ====================
/**
* 修正待审佣金记录
* POST /api/admin/commission-records/{id}/resolve
*/
static resolveCommissionRecord(
id: number,
params: ResolveCommissionParams
): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/commission-records/${id}/resolve`, params)
}
} }

View File

@@ -9,12 +9,13 @@
import { import {
generateSeriesCode, generateSeriesCode,
generatePackageCode, generatePackageCode,
generateShopCode generateShopCode,
generateCarrierCode
} from '@/utils/codeGenerator' } from '@/utils/codeGenerator'
interface Props { interface Props {
// 编码类型: series(套餐系列) | package(套餐) | shop(店铺) // 编码类型: series(套餐系列) | package(套餐) | shop(店铺) | carrier(运营商)
codeType: 'series' | 'package' | 'shop' codeType: 'series' | 'package' | 'shop' | 'carrier'
// 按钮文字 // 按钮文字
buttonText?: string buttonText?: string
// 表单字段名,用于清除验证 // 表单字段名,用于清除验证
@@ -43,6 +44,9 @@
case 'shop': case 'shop':
code = generateShopCode() code = generateShopCode()
break break
case 'carrier':
code = generateCarrierCode()
break
default: default:
ElMessage.error('未知的编码类型') ElMessage.error('未知的编码类型')
return return

View File

@@ -9,7 +9,8 @@ export const COMMISSION_STATUS_OPTIONS = [
{ label: '已冻结', value: CommissionStatus.FROZEN, type: 'info' as const }, { label: '已冻结', value: CommissionStatus.FROZEN, type: 'info' as const },
{ label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const }, { label: '解冻中', value: CommissionStatus.UNFREEZING, type: 'warning' as const },
{ label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const }, { label: '已发放', value: CommissionStatus.RELEASED, type: 'success' as const },
{ label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const } { label: '已失效', value: CommissionStatus.INVALID, type: 'danger' as const },
{ label: '待人工修正', value: CommissionStatus.PENDING_CORRECTION, type: 'warning' as const }
] ]
// 提现状态选项 // 提现状态选项
@@ -39,12 +40,13 @@ export const WithdrawalStatusMap = {
// ========== 佣金状态映射 ========== // ========== 佣金状态映射 ==========
// 佣金状态映射 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效) // 佣金状态映射 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效, 99:待人工修正)
export const CommissionStatusMap = { export const CommissionStatusMap = {
1: { label: '已冻结', type: 'info' as const, color: '#909399' }, 1: { label: '已冻结', type: 'info' as const, color: '#909399' },
2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' }, 2: { label: '解冻中', type: 'warning' as const, color: '#E6A23C' },
3: { label: '已发放', type: 'success' as const, color: '#67C23A' }, 3: { label: '已发放', type: 'success' as const, color: '#67C23A' },
4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' } 4: { label: '已失效', type: 'danger' as const, color: '#F56C6C' },
99: { label: '待人工修正', type: 'warning' as const, color: '#E6A23C' }
} }
// ========== 提现方式映射 ========== // ========== 提现方式映射 ==========

View File

@@ -721,6 +721,7 @@
"accountNumberRequired": "Please enter account number", "accountNumberRequired": "Please enter account number",
"bankNameRequired": "Please enter bank name", "bankNameRequired": "Please enter bank name",
"rejectReasonRequired": "Please enter reject reason", "rejectReasonRequired": "Please enter reject reason",
"remarkRequired": "Please enter remark",
"minWithdrawalRequired": "Please enter min withdrawal amount", "minWithdrawalRequired": "Please enter min withdrawal amount",
"maxWithdrawalRequired": "Please enter max withdrawal amount", "maxWithdrawalRequired": "Please enter max withdrawal amount",
"feeRateRequired": "Please enter fee rate", "feeRateRequired": "Please enter fee rate",

View File

@@ -719,6 +719,7 @@
"accountNumberRequired": "请输入账号", "accountNumberRequired": "请输入账号",
"bankNameRequired": "请输入银行名称", "bankNameRequired": "请输入银行名称",
"rejectReasonRequired": "请输入拒绝原因", "rejectReasonRequired": "请输入拒绝原因",
"remarkRequired": "请输入备注",
"minWithdrawalRequired": "请输入最低提现金额", "minWithdrawalRequired": "请输入最低提现金额",
"maxWithdrawalRequired": "请输入最高提现金额", "maxWithdrawalRequired": "请输入最高提现金额",
"feeRateRequired": "请输入手续费率", "feeRateRequired": "请输入手续费率",

View File

@@ -17,8 +17,7 @@ export enum NetworkStatus {
// 实名状态 // 实名状态
export enum RealNameStatus { export enum RealNameStatus {
NOT_VERIFIED = 0, // 未实名 NOT_VERIFIED = 0, // 未实名
VERIFYING = 1, // 实名 VERIFIED = 1 // 实名
VERIFIED = 2 // 已实名
} }
// 保护期状态 // 保护期状态
@@ -55,7 +54,7 @@ export interface AssetResolveResponse {
shop_name: string // 所属店铺名称 shop_name: string // 所属店铺名称
series_id: number // 套餐系列 ID series_id: number // 套餐系列 ID
series_name: string // 套餐系列名称 series_name: string // 套餐系列名称
real_name_status: RealNameStatus // 实名状态0 未实名 / 1 实名中 / 2 已实名 real_name_status: RealNameStatus // 实名状态0 未实名 / 1 已实名
network_status?: NetworkStatus // 网络状态0 停机 / 1 开机(仅 card network_status?: NetworkStatus // 网络状态0 停机 / 1 开机(仅 card
current_package: string // 当前套餐名称(无则空) current_package: string // 当前套餐名称(无则空)
package_total_mb: number // 当前套餐总虚流量 MB package_total_mb: number // 当前套餐总虚流量 MB
@@ -93,6 +92,11 @@ export interface AssetResolveResponse {
device_type?: string // 设备类型 device_type?: string // 设备类型
max_sim_slots?: number // 最大插槽数 max_sim_slots?: number // 最大插槽数
manufacturer?: string // 制造商 manufacturer?: string // 制造商
online_status?: number // 在线状态0=未知, 1=在线, 2=离线(仅 device
last_online_time?: string | null // 最后在线时间(仅 device
software_version?: string // 固件版本号(仅 device
switch_mode?: string // 切卡模式:"0"=自动, "1"=手动(仅 device
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间(仅 device
} }
/** /**
@@ -105,10 +109,49 @@ export interface AssetBoundCard {
network_status: NetworkStatus // 网络状态 network_status: NetworkStatus // 网络状态
real_name_status: RealNameStatus // 实名状态 real_name_status: RealNameStatus // 实名状态
slot_position: number // 插槽位置 slot_position: number // 插槽位置
is_current?: boolean // 是否为设备当前使用的卡
} }
// ========== 资产实时状态响应 ========== // ========== 资产实时状态响应 ==========
/**
* 设备 Gateway 实时信息
*/
export interface DeviceGatewayInfo {
online_status?: number // 1=在线, 2=离线
battery_level?: number | null // 电量 %(无电池时 null
status?: number // 设备状态 1=正常, 0=禁用
run_time?: string // 本次开机时长(秒)
connect_time?: string // 本次联网时长(秒)
last_online_time?: string // 设备最后在线时间
last_update_time?: string // 信息最后更新时间
rsrp?: number // 信号接收功率(dBm)
rsrq?: number // 信号接收质量(dB)
rssi?: string // 信号强度
sinr?: number // 信噪比(dB)
ssid?: string // WiFi 热点名称
wifi_enabled?: boolean // WiFi 开关
wifi_password?: string // WiFi 密码
ip_address?: string // IP
wan_ip?: string // 基站分配 IPv4
lan_ip?: string // 局域网网关 IP
mac_address?: string // MAC 地址
daily_usage?: string // 日使用流量(字节)
dl_stats?: string // 本次开机下载(字节)
ul_stats?: string // 本次开机上传(字节)
limit_speed?: number // 限速(KB/s)0=不限
current_iccid?: string // 当前使用卡 ICCID
max_clients?: number // 最大连接客户端数
software_version?: string // 固件版本
switch_mode?: string // 切卡模式
sync_interval?: number // 上报周期(秒)
device_id?: string // Gateway 设备ID
device_name?: string // Gateway 设备名称
device_type?: string // Gateway 设备类型
imei?: string
imsi?: string
}
/** /**
* 资产实时状态响应 * 资产实时状态响应
* 对应接口GET /api/admin/assets/:asset_type/:id/realtime-status * 对应接口GET /api/admin/assets/:asset_type/:id/realtime-status
@@ -126,6 +169,12 @@ export interface AssetRealtimeStatusResponse {
// ===== 设备专属字段 ===== // ===== 设备专属字段 =====
device_protect_status?: DeviceProtectStatus // 保护期(仅 device device_protect_status?: DeviceProtectStatus // 保护期(仅 device
cards?: AssetBoundCard[] // 所有绑定卡的状态(仅 device cards?: AssetBoundCard[] // 所有绑定卡的状态(仅 device
online_status?: number // 在线状态(仅 device
last_online_time?: string | null // 最后在线时间(仅 device
software_version?: string // 固件版本号(仅 device
switch_mode?: string // 切卡模式(仅 device
last_gateway_sync_at?: string | null // 最后同步时间(仅 device
device_realtime?: DeviceGatewayInfo | null // Gateway 实时数据(仅 deviceGateway 不可达时为 null
} }
/** /**
@@ -161,6 +210,25 @@ export interface AssetPackageUsageRecord {
created_at?: string // 创建时间 created_at?: string // 创建时间
} }
/**
* 资产套餐列表分页响应
* 对应接口GET /api/admin/assets/:asset_type/:id/packages?page=1&page_size=50
*/
export interface AssetPackageListResponse {
total: number // 总记录数
page: number // 当前页码
page_size: number // 每页数量
items: AssetPackageUsageRecord[] // 套餐记录列表
}
/**
* 资产套餐查询参数
*/
export interface AssetPackageParams {
page?: number // 页码默认1
page_size?: number // 每页数量默认50
}
/** /**
* 当前套餐响应(结构同套餐使用记录单项) * 当前套餐响应(结构同套餐使用记录单项)
* 对应接口GET /api/admin/assets/:asset_type/:id/current-package * 对应接口GET /api/admin/assets/:asset_type/:id/current-package

View File

@@ -9,7 +9,8 @@ export enum CommissionStatus {
FROZEN = 1, // 已冻结 FROZEN = 1, // 已冻结
UNFREEZING = 2, // 解冻中 UNFREEZING = 2, // 解冻中
RELEASED = 3, // 已发放 RELEASED = 3, // 已发放
INVALID = 4 // 已失效 INVALID = 4, // 已失效
PENDING_CORRECTION = 99 // 待人工修正
} }
// 提现状态 // 提现状态
@@ -92,7 +93,7 @@ export interface ApproveWithdrawalParams {
*/ */
export interface RejectWithdrawalParams { export interface RejectWithdrawalParams {
reject_reason: string // 拒绝原因 reject_reason: string // 拒绝原因
remark?: string // 备注 remark: string // 备注(必填)
} }
// ==================== 提现配置相关 ==================== // ==================== 提现配置相关 ====================
@@ -294,3 +295,19 @@ export interface DailyCommissionStatsItem {
total_amount: number // 当日总收入(分) total_amount: number // 当日总收入(分)
total_count: number // 当日总笔数 total_count: number // 当日总笔数
} }
// ==================== 佣金记录修正相关 ====================
/**
* 佣金记录修正动作类型
*/
export type CommissionResolveAction = 'release' | 'invalidate'
/**
* 佣金记录修正请求参数
*/
export interface ResolveCommissionParams {
action: CommissionResolveAction // 操作类型release入账/ invalidate作废
amount?: number // 佣金金额action=release 时必填
remark?: string // 备注可选最多500字符
}

View File

@@ -14,6 +14,16 @@ export enum DeviceStatus {
DEACTIVATED = 4 // 已停用 DEACTIVATED = 4 // 已停用
} }
// 设备在线状态枚举
export enum DeviceOnlineStatus {
UNKNOWN = 0, // 未知
ONLINE = 1, // 在线
OFFLINE = 2 // 离线
}
// 切卡模式
export type SwitchMode = '0' | '1' // 0=自动, 1=手动
// ========== 设备基础类型 ========== // ========== 设备基础类型 ==========
// 设备信息 // 设备信息
@@ -35,6 +45,11 @@ export interface Device {
created_at: string // 创建时间 created_at: string // 创建时间
updated_at: string // 更新时间 updated_at: string // 更新时间
series_id?: number | null // 套餐系列ID series_id?: number | null // 套餐系列ID
online_status?: DeviceOnlineStatus // 在线状态0=未知, 1=在线, 2=离线
last_online_time?: string | null // 最后在线时间
software_version?: string // 固件版本号
switch_mode?: SwitchMode // 切卡模式:"0"=自动, "1"=手动
last_gateway_sync_at?: string | null // 最后 sync-info 同步时间
} }
// 设备查询参数 // 设备查询参数
@@ -71,6 +86,7 @@ export interface DeviceCardBinding {
slot_position: number // 插槽位置 (1-4) slot_position: number // 插槽位置 (1-4)
status: number // 卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用) status: number // 卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)
bind_time: string | null // 绑定时间 bind_time: string | null // 绑定时间
is_current?: boolean // 是否为设备当前使用的卡
} }
// 设备绑定的卡列表响应 // 设备绑定的卡列表响应

View File

@@ -96,6 +96,11 @@ export interface CommissionTierInfo {
next_threshold?: number | null // 下一档位阈值 next_threshold?: number | null // 下一档位阈值
} }
/**
* 套餐到期计时基准枚举
*/
export type ExpiryBase = 'from_activation' | 'from_purchase'
/** /**
* 套餐响应 * 套餐响应
*/ */
@@ -114,7 +119,7 @@ export interface PackageResponse {
virtual_data_mb?: number // 虚流量额度MB virtual_data_mb?: number // 虚流量额度MB
virtual_ratio?: number // 虚流量比例real_data_mb / virtual_data_mb。启用虚流量时计算否则为 1.0 virtual_ratio?: number // 虚流量比例real_data_mb / virtual_data_mb。启用虚流量时计算否则为 1.0
enable_virtual_data?: boolean // 是否启用虚流量 enable_virtual_data?: boolean // 是否启用虚流量
enable_realname_activation?: boolean // 是否启用实名激活 (true:需实名后激活, false:立即激活) expiry_base?: ExpiryBase // 到期计时基准: from_activation(实名后开始计时) / from_purchase(购买即开始计时)
cost_price?: number // 成本价(分) cost_price?: number // 成本价(分)
suggested_retail_price?: number // 建议零售价(分) suggested_retail_price?: number // 建议零售价(分)
current_commission_rate?: string // 当前返佣比例(仅代理用户可见) current_commission_rate?: string // 当前返佣比例(仅代理用户可见)
@@ -155,7 +160,7 @@ export interface CreatePackageRequest {
real_data_mb?: number | null // 真流量额度MB可选 real_data_mb?: number | null // 真流量额度MB可选
virtual_data_mb?: number | null // 虚流量额度MB可选 virtual_data_mb?: number | null // 虚流量额度MB可选
enable_virtual_data?: boolean // 是否启用虚流量,可选 enable_virtual_data?: boolean // 是否启用虚流量,可选
enable_realname_activation?: boolean | null // 是否启用实名激活 (true:需实名后激活, false:立即激活),可选 expiry_base?: ExpiryBase | null // 到期计时基准: from_activation(实名后开始计时) / from_purchase(购买即开始计时),可选
cost_price: number // 成本价(分),必填 cost_price: number // 成本价(分),必填
suggested_retail_price?: number | null // 建议零售价(分),可选 suggested_retail_price?: number | null // 建议零售价(分),可选
} }
@@ -174,7 +179,7 @@ export interface UpdatePackageRequest {
real_data_mb?: number | null // 真流量额度MB可选 real_data_mb?: number | null // 真流量额度MB可选
virtual_data_mb?: number | null // 虚流量额度MB可选 virtual_data_mb?: number | null // 虚流量额度MB可选
enable_virtual_data?: boolean | null // 是否启用虚流量,可选 enable_virtual_data?: boolean | null // 是否启用虚流量,可选
enable_realname_activation?: boolean | null // 是否启用实名激活,可选 expiry_base?: ExpiryBase | null // 到期计时基准,可选
cost_price?: number | null // 成本价(分),可选 cost_price?: number | null // 成本价(分),可选
suggested_retail_price?: number | null // 建议零售价(分),可选 suggested_retail_price?: number | null // 建议零售价(分),可选
} }

View File

@@ -67,3 +67,25 @@ export function generateShopCode(): string {
return `SHOP${year}${month}${day}${hours}${minutes}${seconds}${randomChars}` return `SHOP${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
} }
/**
* 生成运营商编码
* 规则: CARRIER + 年月日时分秒 + 4位随机数
* 示例: CARRIER20260328143025ABCD
*/
export function generateCarrierCode(): string {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
// 生成4位随机大写字母
const randomChars = Array.from({ length: 4 }, () =>
String.fromCharCode(65 + Math.floor(Math.random() * 26))
).join('')
return `CARRIER${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
}

View File

@@ -403,7 +403,14 @@
</div> </div>
<div v-else> <div v-else>
<ElTable :data="deviceCards" border style="width: 100%"> <ElTable :data="deviceCards" border style="width: 100%">
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center" /> <ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center">
<template #default="{ row }">
<div style="display: flex; align-items: center; justify-content: center; gap: 4px">
<span>{{ row.slot_position }}</span>
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn prop="iccid" label="ICCID" min-width="180" /> <ElTableColumn prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn prop="msisdn" label="接入号" width="140" /> <ElTableColumn prop="msisdn" label="接入号" width="140" />
<ElTableColumn prop="status" label="状态" width="100" align="center"> <ElTableColumn prop="status" label="状态" width="100" align="center">
@@ -524,7 +531,7 @@
<ElOption <ElOption
v-for="card in deviceBindingCards" v-for="card in deviceBindingCards"
:key="card.iccid" :key="card.iccid"
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`" :label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}${card.is_current ? ' (当前)' : ''}`"
:value="card.iccid" :value="card.iccid"
> >
<div style="display: flex; justify-content: space-between; align-items: center"> <div style="display: flex; justify-content: space-between; align-items: center">
@@ -858,6 +865,11 @@
{ label: '最大插槽数', prop: 'max_sim_slots' }, { label: '最大插槽数', prop: 'max_sim_slots' },
{ label: '已绑定卡数', prop: 'bound_card_count' }, { label: '已绑定卡数', prop: 'bound_card_count' },
{ label: '状态', prop: 'status' }, { 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' },
{ label: '批次号', prop: 'batch_no' }, { label: '批次号', prop: 'batch_no' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
@@ -914,6 +926,14 @@
const res = await DeviceService.getDeviceCards(deviceId) const res = await DeviceService.getDeviceCards(deviceId)
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
deviceCards.value = res.data.bindings || [] deviceCards.value = res.data.bindings || []
console.log('设备绑定的卡列表:', deviceCards.value)
// 🔧 临时调试强制第一张卡显示为当前卡方便测试UI效果
// 测试完成后请删除此代码
if (deviceCards.value.length > 0) {
deviceCards.value[0].is_current = true
console.log('🧪 测试模式:已将第一张卡标记为当前卡')
}
} }
} catch (error) { } catch (error) {
console.error('获取设备绑定的卡列表失败:', error) console.error('获取设备绑定的卡列表失败:', error)
@@ -1135,6 +1155,50 @@
return h(ElTag, { type: status.type }, () => status.text) return h(ElTag, { type: status.type }, () => status.text)
} }
}, },
{
prop: 'online_status',
label: '在线状态',
width: 100,
formatter: (row: Device) => {
const onlineStatusMap: Record<number, { text: string; type: any }> = {
0: { text: '未知', type: 'info' },
1: { text: '在线', type: 'success' },
2: { text: '离线', type: 'danger' }
}
const status = onlineStatusMap[row.online_status ?? 0] || { text: '未知', type: 'info' }
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
prop: 'software_version',
label: '固件版本',
width: 120,
formatter: (row: Device) => row.software_version || '-'
},
{
prop: 'switch_mode',
label: '切卡模式',
width: 100,
formatter: (row: Device) => {
const modeMap: Record<string, string> = {
'0': '自动',
'1': '手动'
}
return modeMap[row.switch_mode || ''] || '-'
}
},
{
prop: 'last_online_time',
label: '最后在线时间',
width: 180,
formatter: (row: Device) => row.last_online_time ? formatDateTime(row.last_online_time) : '-'
},
{
prop: 'last_gateway_sync_at',
label: '最后同步时间',
width: 180,
formatter: (row: Device) => row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-'
},
{ {
prop: 'batch_no', prop: 'batch_no',
label: '批次号', label: '批次号',
@@ -1717,6 +1781,14 @@
const res = await DeviceService.getDeviceCards(device.id) const res = await DeviceService.getDeviceCards(device.id)
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
deviceBindingCards.value = res.data.bindings || [] 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) { if (deviceBindingCards.value.length === 0) {
ElMessage.warning('该设备暂无绑定的SIM卡') ElMessage.warning('该设备暂无绑定的SIM卡')
} }

View File

@@ -63,16 +63,36 @@
{{ deviceDetail.status_name }} {{ deviceDetail.status_name }}
</ElTag> </ElTag>
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusTagType(deviceDetail.online_status)">
{{ getOnlineStatusText(deviceDetail.online_status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">{{ <ElDescriptionsItem label="店铺名称">{{
deviceDetail.shop_name || '--' deviceDetail.shop_name || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">{{
deviceDetail.software_version || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeText(deviceDetail.switch_mode)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem> <ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">{{
deviceDetail.last_online_time || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最后同步时间">{{
deviceDetail.last_gateway_sync_at || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="激活时间">{{ <ElDescriptionsItem label="激活时间">{{
deviceDetail.activated_at || '--' deviceDetail.activated_at || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem> <ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem> <ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
<ElDescriptionsItem></ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
</ElCard> </ElCard>
@@ -131,6 +151,35 @@
} }
return typeMap[status] || 'info' return typeMap[status] || 'info'
} }
// 获取在线状态标签类型
const getOnlineStatusTagType = (status?: number) => {
const typeMap: Record<number, any> = {
0: 'info', // 未知
1: 'success', // 在线
2: 'danger' // 离线
}
return typeMap[status ?? 0] || 'info'
}
// 获取在线状态文本
const getOnlineStatusText = (status?: number) => {
const textMap: Record<number, string> = {
0: '未知',
1: '在线',
2: '离线'
}
return textMap[status ?? 0] || '未知'
}
// 获取切卡模式文本
const getSwitchModeText = (mode?: string) => {
const modeMap: Record<string, string> = {
'0': '自动',
'1': '手动'
}
return modeMap[mode || ''] || '--'
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -75,7 +75,7 @@
<p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p> <p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p>
<p <p
>4. >4.
必填列virtual_no设备号device_name设备名称device_model设备型号device_type设备类型</p 必填列virtual_no设备号device_name设备名称device_model设备型号device_type设备类型imei设备IMEI号</p
> >
<p <p
>5. 可选列manufacturer制造商max_sim_slots最大插槽数默认4iccid_1 ~ >5. 可选列manufacturer制造商max_sim_slots最大插槽数默认4iccid_1 ~
@@ -445,6 +445,7 @@
device_name: '智能水表01', device_name: '智能水表01',
device_model: 'WM-2000', device_model: 'WM-2000',
device_type: '智能水表', device_type: '智能水表',
imei: '860123456789012',
manufacturer: '华为', manufacturer: '华为',
max_sim_slots: 4, max_sim_slots: 4,
iccid_1: '89860123456789012345', iccid_1: '89860123456789012345',
@@ -457,6 +458,7 @@
device_name: 'GPS定位器01', device_name: 'GPS定位器01',
device_model: 'GPS-3000', device_model: 'GPS-3000',
device_type: '定位设备', device_type: '定位设备',
imei: '860123456789013',
manufacturer: '小米', manufacturer: '小米',
max_sim_slots: 2, max_sim_slots: 2,
iccid_1: '89860123456789012346', iccid_1: '89860123456789012346',
@@ -476,6 +478,7 @@
{ wch: 20 }, // device_name { wch: 20 }, // device_name
{ wch: 15 }, // device_model { wch: 15 }, // device_model
{ wch: 15 }, // device_type { wch: 15 }, // device_type
{ wch: 18 }, // imei
{ wch: 15 }, // manufacturer { wch: 15 }, // manufacturer
{ wch: 15 }, // max_sim_slots { wch: 15 }, // max_sim_slots
{ wch: 22 }, // iccid_1 { wch: 22 }, // iccid_1

View File

@@ -1715,16 +1715,13 @@
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据 // 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid) const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的实名状态real_name_status: 0未实名 1实名中 2已实名 // 直接使用 resolveAsset 返回的实名状态real_name_status: 0未实名 1已实名
let statusText = '未知' let statusText = '未知'
switch (res.data.real_name_status) { switch (res.data.real_name_status) {
case 0: case 0:
statusText = '未实名' statusText = '未实名'
break break
case 1: case 1:
statusText = '实名中'
break
case 2:
statusText = '已实名' statusText = '已实名'
break break
} }

View File

@@ -64,11 +64,20 @@
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElFormItem label="运营商编码" prop="carrier_code"> <ElFormItem label="运营商编码" prop="carrier_code">
<div style="display: flex; gap: 8px">
<ElInput <ElInput
v-model="form.carrier_code" v-model="form.carrier_code"
placeholder="请输入运营商编码" placeholder="请输入运营商编码或点击生成"
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit'"
clearable
style="flex: 1"
/> />
<CodeGeneratorButton
v-if="dialogType === 'add'"
code-type="carrier"
@generated="handleCodeGenerated"
/>
</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="运营商名称" prop="carrier_name"> <ElFormItem label="运营商名称" prop="carrier_name">
<ElInput v-model="form.carrier_name" placeholder="请输入运营商名称" /> <ElInput v-model="form.carrier_name" placeholder="请输入运营商名称" />
@@ -150,6 +159,7 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue' import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { import {
@@ -441,6 +451,15 @@
}) })
} }
// 处理编码生成
const handleCodeGenerated = (code: string) => {
form.carrier_code = code
// 清除该字段的验证错误提示
nextTick(() => {
formRef.value?.clearValidate('carrier_code')
})
}
// 删除运营商 // 删除运营商
const deleteCarrier = (row: any) => { const deleteCarrier = (row: any) => {
ElMessageBox.confirm(`确定删除运营商 ${row.carrier_name} 吗?`, '删除确认', { ElMessageBox.confirm(`确定删除运营商 ${row.carrier_name} 吗?`, '删除确认', {

View File

@@ -151,6 +151,23 @@
{{ formatDateTime(scope.row.created_at) }} {{ formatDateTime(scope.row.created_at) }}
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="操作" width="150" fixed="right">
<template #default="scope">
<div v-if="scope.row.status === 99" style="display: flex; gap: 8px">
<ArtButtonTable
text="入账"
iconColor="#67C23A"
@click="handleResolveCommission(scope.row, 'release')"
/>
<ArtButtonTable
text="作废"
iconColor="#F56C6C"
@click="handleResolveCommission(scope.row, 'invalidate')"
/>
</div>
<span v-else>-</span>
</template>
</ElTableColumn>
</template> </template>
</ArtTable> </ArtTable>
</ElTabPane> </ElTabPane>
@@ -233,6 +250,69 @@
</ElTabPane> </ElTabPane>
</ElTabs> </ElTabs>
</ElDrawer> </ElDrawer>
<!-- 佣金修正对话框 -->
<ElDialog
v-model="resolveDialogVisible"
:title="resolveAction === 'release' ? '佣金入账' : '佣金作废'"
width="500px"
>
<ElAlert
:title="
resolveAction === 'release'
? '确认将该笔待审佣金记录入账'
: '确认将该笔待审佣金记录作废'
"
:type="resolveAction === 'release' ? 'success' : 'warning'"
style="margin-bottom: 16px"
:closable="false"
/>
<ElForm
ref="resolveFormRef"
:model="resolveForm"
:rules="resolveRules"
label-width="100px"
>
<ElFormItem v-if="resolveAction === 'release'" label="入账金额" prop="amount">
<ElInputNumber
v-model="resolveForm.amount"
:min="0"
:step="100"
:precision="0"
controls-position="right"
style="width: 100%"
placeholder="请输入入账金额(分)"
/>
<div style="color: var(--el-text-color-secondary); font-size: 12px; margin-top: 4px">
金额单位为分例如100 = 10000
</div>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput
v-model="resolveForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注可选最多500字符"
maxlength="500"
show-word-limit
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resolveDialogVisible = false">取消</ElButton>
<ElButton
:type="resolveAction === 'release' ? 'success' : 'warning'"
@click="handleResolveSubmit"
:loading="resolveSubmitLoading"
>
确认
</ElButton>
</div>
</template>
</ElDialog>
</div> </div>
</template> </template>
@@ -240,11 +320,13 @@
import { h, watch, onBeforeUnmount } from 'vue' import { h, watch, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { CommissionService } from '@/api/modules' import { CommissionService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus' import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { import type {
ShopCommissionSummaryItem, ShopCommissionSummaryItem,
ShopCommissionRecordItem, ShopCommissionRecordItem,
WithdrawalRequestItem WithdrawalRequestItem,
CommissionResolveAction
} from '@/types/api/commission' } from '@/types/api/commission'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
@@ -321,6 +403,34 @@
total: 0 total: 0
}) })
// 佣金修正对话框状态
const resolveDialogVisible = ref(false)
const resolveFormRef = ref<FormInstance>()
const resolveSubmitLoading = ref(false)
const resolveAction = ref<CommissionResolveAction>('release')
const currentCommissionId = ref<number>(0)
const resolveForm = reactive({
amount: undefined as number | undefined,
remark: ''
})
// 佣金修正表单验证规则
const resolveRules = computed<FormRules>(() => ({
amount:
resolveAction.value === 'release'
? [
{ required: true, message: '请输入入账金额', trigger: 'blur' },
{
type: 'number',
min: 1,
message: '入账金额必须大于0',
trigger: 'blur'
}
]
: [],
remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }]
}))
// 列配置 // 列配置
const columnOptions = [ const columnOptions = [
{ label: '店铺编码', prop: 'shop_code' }, { label: '店铺编码', prop: 'shop_code' },
@@ -624,6 +734,68 @@
// 暂无其他菜单项 // 暂无其他菜单项
} }
// 处理佣金修正
const handleResolveCommission = (row: ShopCommissionRecordItem, action: CommissionResolveAction) => {
currentCommissionId.value = row.id
resolveAction.value = action
resolveForm.amount = undefined
resolveForm.remark = ''
resolveDialogVisible.value = true
}
// 提交佣金修正
const handleResolveSubmit = async () => {
if (!resolveFormRef.value) return
await resolveFormRef.value.validate(async (valid) => {
if (valid) {
const actionText = resolveAction.value === 'release' ? '入账' : '作废'
const confirmText =
resolveAction.value === 'release'
? `确认将该笔佣金记录入账 ${formatMoney(resolveForm.amount || 0)} 吗?`
: '确认将该笔佣金记录作废吗?'
try {
await ElMessageBox.confirm(confirmText, '确认操作', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
resolveSubmitLoading.value = true
try {
const params: any = {
action: resolveAction.value
}
if (resolveAction.value === 'release' && resolveForm.amount) {
params.amount = resolveForm.amount
}
if (resolveForm.remark) {
params.remark = resolveForm.remark
}
await CommissionService.resolveCommissionRecord(currentCommissionId.value, params)
ElMessage.success(`${actionText}成功`)
resolveDialogVisible.value = false
resolveFormRef.value?.resetFields()
// 刷新佣金明细列表
loadCommissionRecords()
} catch (error: any) {
console.error(error)
ElMessage.error(error?.message || `${actionText}失败`)
} finally {
resolveSubmitLoading.value = false
}
} catch {
// 用户取消
}
}
})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -201,6 +201,9 @@
const rejectRules = reactive<FormRules>({ const rejectRules = reactive<FormRules>({
reject_reason: [ reject_reason: [
{ required: true, message: t('commission.validation.rejectReasonRequired'), trigger: 'blur' } { required: true, message: t('commission.validation.rejectReasonRequired'), trigger: 'blur' }
],
remark: [
{ required: true, message: t('commission.validation.remarkRequired'), trigger: 'blur' }
] ]
}) })
@@ -402,7 +405,7 @@
try { try {
await CommissionService.rejectWithdrawal(currentWithdrawalId.value, { await CommissionService.rejectWithdrawal(currentWithdrawalId.value, {
reject_reason: rejectForm.reject_reason, reject_reason: rejectForm.reject_reason,
remark: rejectForm.remark || undefined remark: rejectForm.remark
}) })
ElMessage.success(t('commission.messages.rejectSuccess')) ElMessage.success(t('commission.messages.rejectSuccess'))
rejectDialogVisible.value = false rejectDialogVisible.value = false

View File

@@ -156,6 +156,28 @@
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="实名时间">暂未接入</ElDescriptionsItem> <ElDescriptionsItem label="实名时间">暂未接入</ElDescriptionsItem>
<ElDescriptionsItem label="信号强度">暂未接入</ElDescriptionsItem> <ElDescriptionsItem label="信号强度">暂未接入</ElDescriptionsItem>
<ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusType(cardInfo?.online_status)" size="small">
{{ getOnlineStatusName(cardInfo?.online_status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">{{
cardInfo?.software_version || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeName(cardInfo?.switch_mode)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">{{
formatDateTime(cardInfo?.last_online_time) || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最后同步时间">{{
formatDateTime(cardInfo?.last_gateway_sync_at) || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="制造商">{{
cardInfo?.manufacturer || '--'
}}</ElDescriptionsItem>
<!--<ElDescriptionsItem label="制造商">{{--> <!--<ElDescriptionsItem label="制造商">{{-->
<!-- cardInfo?.manufacturer || '&#45;&#45;'--> <!-- cardInfo?.manufacturer || '&#45;&#45;'-->
<!--}}</ElDescriptionsItem>--> <!--}}</ElDescriptionsItem>-->
@@ -187,7 +209,16 @@
> >
<ElTableColumn prop="iccid" label="ICCID" min-width="180" /> <ElTableColumn prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn prop="msisdn" label="MSISDN" min-width="120" /> <ElTableColumn prop="msisdn" label="MSISDN" min-width="120" />
<ElTableColumn prop="slot_position" label="卡槽位置" width="100" align="center" /> <ElTableColumn prop="slot_position" label="卡槽位置" width="120" align="center">
<template #default="scope">
<div
style="display: flex; align-items: center; justify-content: center; gap: 4px"
>
<span>{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="网络状态" width="100" align="center"> <ElTableColumn label="网络状态" width="100" align="center">
<template #default="scope"> <template #default="scope">
<ElTag <ElTag
@@ -207,6 +238,163 @@
</ElTableColumn> </ElTableColumn>
</ElTable> </ElTable>
<ElEmpty v-else description="暂无绑定卡" :image-size="80" style="margin-top: 16px" /> <ElEmpty v-else description="暂无绑定卡" :image-size="80" style="margin-top: 16px" />
<!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="deviceRealtime" style="margin-top: 16px">
<ElDescriptions :column="3" border size="small">
<!-- 基本状态 -->
<ElDescriptionsItem label="在线状态">
<ElTag
:type="deviceRealtime.online_status === 1 ? 'success' : 'danger'"
size="small"
>
{{ deviceRealtime.online_status === 1 ? '在线' : '离线' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="设备状态">
<ElTag :type="deviceRealtime.status === 1 ? 'success' : 'info'" size="small">
{{ deviceRealtime.status === 1 ? '正常' : '禁用' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="电量">
{{
deviceRealtime.battery_level !== null &&
deviceRealtime.battery_level !== undefined
? `${deviceRealtime.battery_level}%`
: '--'
}}
</ElDescriptionsItem>
<!-- 时间信息 -->
<ElDescriptionsItem label="本次开机时长">
{{ formatDuration(deviceRealtime.run_time) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="本次联网时长">
{{ formatDuration(deviceRealtime.connect_time) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">
{{ formatDateTime(deviceRealtime.last_online_time) || '--' }}
</ElDescriptionsItem>
<!-- 信号信息 -->
<ElDescriptionsItem label="信号强度(RSSI)">
{{ deviceRealtime.rssi || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="接收功率(RSRP)">
{{ deviceRealtime.rsrp ? `${deviceRealtime.rsrp} dBm` : '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="接收质量(RSRQ)">
{{ deviceRealtime.rsrq ? `${deviceRealtime.rsrq} dB` : '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="信噪比(SINR)">
{{ deviceRealtime.sinr ? `${deviceRealtime.sinr} dB` : '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最大客户端数">
{{ deviceRealtime.max_clients || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="当前使用ICCID">
<span
v-if="deviceRealtime.current_iccid"
style="
color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline;
"
@click="handleViewCardDetail(deviceRealtime.current_iccid)"
>
{{ deviceRealtime.current_iccid }}
</span>
<span v-else>--</span>
</ElDescriptionsItem>
<!-- WiFi 信息 -->
<ElDescriptionsItem label="WiFi 状态">
<ElTag :type="deviceRealtime.wifi_enabled ? 'success' : 'info'" size="small">
{{ deviceRealtime.wifi_enabled ? '已开启' : '已关闭' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="WiFi 名称">
{{ deviceRealtime.ssid || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="WiFi 密码">
{{ deviceRealtime.wifi_password || '--' }}
</ElDescriptionsItem>
<!-- 网络信息 -->
<ElDescriptionsItem label="IP 地址">
{{ deviceRealtime.ip_address || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="WAN IP">
{{ deviceRealtime.wan_ip || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="LAN IP">
{{ deviceRealtime.lan_ip || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="MAC 地址">
{{ deviceRealtime.mac_address || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="IMEI">
{{ deviceRealtime.imei || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="IMSI">
{{ deviceRealtime.imsi || '--' }}
</ElDescriptionsItem>
<!-- 流量信息 -->
<ElDescriptionsItem label="今日流量">
{{ formatDataSize(Number(deviceRealtime.daily_usage || 0) / (1024 * 1024)) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="本次下载">
{{ formatDataSize(Number(deviceRealtime.dl_stats || 0) / (1024 * 1024)) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="本次上传">
{{ formatDataSize(Number(deviceRealtime.ul_stats || 0) / (1024 * 1024)) }}
</ElDescriptionsItem>
<!-- 设备配置 -->
<ElDescriptionsItem label="限速">
{{
deviceRealtime.limit_speed ? `${deviceRealtime.limit_speed} KB/s` : '不限速'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">
{{ deviceRealtime.software_version || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">
{{
deviceRealtime.switch_mode === '0'
? '自动'
: deviceRealtime.switch_mode === '1'
? '手动'
: '--'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="上报周期">
{{ deviceRealtime.sync_interval ? `${deviceRealtime.sync_interval}` : '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="设备ID">
{{ deviceRealtime.device_id || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="信息更新时间">
{{ formatDateTime(deviceRealtime.last_update_time) || '--' }}
</ElDescriptionsItem>
</ElDescriptions>
</div>
<ElAlert
v-else
title="设备实时信息不可用"
type="warning"
:closable="false"
style="margin-top: 16px"
>
<template #default>
<div>Gateway 不可达或设备离线无法获取实时信息</div>
</template>
</ElAlert>
</template> </template>
<!-- IoT卡操作按钮 --> <!-- IoT卡操作按钮 -->
@@ -1154,6 +1342,9 @@
// 卡片信息 - 默认为null,等待查询 // 卡片信息 - 默认为null,等待查询
const cardInfo = ref<any>(null) const cardInfo = ref<any>(null)
// 设备实时信息
const deviceRealtime = ref<any>(null)
// 当前套餐错误信息 // 当前套餐错误信息
const currentPackageErrorMsg = ref<string>('') const currentPackageErrorMsg = ref<string>('')
@@ -1237,6 +1428,11 @@
bound_card_count: data.bound_card_count || 0, bound_card_count: data.bound_card_count || 0,
cards: data.cards || [], cards: data.cards || [],
device_protect_status: data.device_protect_status, device_protect_status: data.device_protect_status,
online_status: data.online_status,
last_online_time: data.last_online_time,
software_version: data.software_version,
switch_mode: data.switch_mode,
last_gateway_sync_at: data.last_gateway_sync_at,
// 流量/套餐信息 // 流量/套餐信息
current_package: data.current_package || '', current_package: data.current_package || '',
@@ -1284,10 +1480,13 @@
// 加载套餐列表 // 加载套餐列表
const loadPackageList = async (assetType: string, assetId: number) => { const loadPackageList = async (assetType: string, assetId: number) => {
try { try {
const response = await AssetService.getAssetPackages(assetType, assetId) const response = await AssetService.getAssetPackages(assetType, assetId, {
page: 1,
page_size: 50
})
if (response.code === 0 && response.data) { if (response.code === 0 && response.data) {
// 直接使用API返回的数据结构 // 使用分页响应中的 items 数组
cardInfo.value.packageList = response.data cardInfo.value.packageList = response.data.items || []
} }
} catch (error) { } catch (error) {
console.error('获取套餐列表失败:', error) console.error('获取套餐列表失败:', error)
@@ -1301,7 +1500,8 @@
currentPackageErrorMsg.value = '' currentPackageErrorMsg.value = ''
const response = await AssetService.getCurrentPackage(assetType, assetId) const response = await AssetService.getCurrentPackage(assetType, assetId)
if (response.code === 0 && response.data) { if (response.code === 0) {
if (response.data) {
const pkg = response.data const pkg = response.data
// 保存完整的当前套餐数据 // 保存完整的当前套餐数据
cardInfo.value.currentPackageDetail = { cardInfo.value.currentPackageDetail = {
@@ -1333,6 +1533,10 @@
pkg.virtual_limit_mb > 0 pkg.virtual_limit_mb > 0
? `${((pkg.virtual_used_mb / pkg.virtual_limit_mb) * 100).toFixed(2)}%` ? `${((pkg.virtual_used_mb / pkg.virtual_limit_mb) * 100).toFixed(2)}%`
: '0.00%' : '0.00%'
} else {
// code 为 0 但 data 为空,表示暂无当前生效套餐(正常情况)
currentPackageErrorMsg.value = '暂无当前生效套餐'
}
} else { } else {
// 接口返回 code 不为 0,保存错误信息 // 接口返回 code 不为 0,保存错误信息
currentPackageErrorMsg.value = response.msg || '获取当前套餐失败' currentPackageErrorMsg.value = response.msg || '获取当前套餐失败'
@@ -1377,6 +1581,26 @@
if (data.cards && data.cards.length > 0) { if (data.cards && data.cards.length > 0) {
cardInfo.value.cards = data.cards cardInfo.value.cards = data.cards
} }
if (data.online_status !== undefined) {
cardInfo.value.online_status = data.online_status
}
if (data.last_online_time !== undefined) {
cardInfo.value.last_online_time = data.last_online_time
}
if (data.software_version !== undefined) {
cardInfo.value.software_version = data.software_version
}
if (data.switch_mode !== undefined) {
cardInfo.value.switch_mode = data.switch_mode
}
if (data.last_gateway_sync_at !== undefined) {
cardInfo.value.last_gateway_sync_at = data.last_gateway_sync_at
}
// 设备实时信息Gateway 数据)
if (data.device_realtime !== undefined) {
deviceRealtime.value = data.device_realtime
console.log('设备实时信息:', deviceRealtime.value)
}
} }
} }
} catch (error) { } catch (error) {
@@ -1455,8 +1679,7 @@
const getRealNameStatusName = (status: number) => { const getRealNameStatusName = (status: number) => {
const statusMap: Record<number, string> = { const statusMap: Record<number, string> = {
0: '未实名', 0: '未实名',
1: '实名中', 1: '实名'
2: '已实名'
} }
return statusMap[status] || '未知' return statusMap[status] || '未知'
} }
@@ -1465,8 +1688,7 @@
const getRealNameStatusType = (status: number) => { const getRealNameStatusType = (status: number) => {
const map: Record<number, any> = { const map: Record<number, any> = {
0: 'info', 0: 'info',
1: 'warning', 1: 'success'
2: 'success'
} }
return map[status] || 'info' return map[status] || 'info'
} }
@@ -1503,6 +1725,65 @@
return map[status || 'none'] || 'info' return map[status || 'none'] || 'info'
} }
// 获取在线状态标签类型
const getOnlineStatusType = (status?: number) => {
const map: Record<number, any> = {
0: 'info', // 未知
1: 'success', // 在线
2: 'danger' // 离线
}
return map[status ?? 0] || 'info'
}
// 获取在线状态名称
const getOnlineStatusName = (status?: number) => {
const map: Record<number, string> = {
0: '未知',
1: '在线',
2: '离线'
}
return map[status ?? 0] || '未知'
}
// 获取切卡模式名称
const getSwitchModeName = (mode?: string) => {
const map: Record<string, string> = {
'0': '自动',
'1': '手动'
}
return map[mode || ''] || '--'
}
// 格式化时长(秒转为时分秒)
const formatDuration = (seconds?: string) => {
if (!seconds) return '--'
const sec = Number(seconds)
if (isNaN(sec)) return '--'
const hours = Math.floor(sec / 3600)
const minutes = Math.floor((sec % 3600) / 60)
const secs = sec % 60
if (hours > 0) {
return `${hours}小时${minutes}${secs}`
} else if (minutes > 0) {
return `${minutes}${secs}`
} else {
return `${secs}`
}
}
// 点击ICCID查看卡详情
const handleViewCardDetail = (iccid: string) => {
if (!iccid) return
// 在新标签页打开卡详情
const route = router.resolve({
path: '/asset-management/single-card',
query: { iccid }
})
window.open(route.href, '_blank')
}
// 获取保护期状态名称 // 获取保护期状态名称
const getProtectStatusName = (status?: string) => { const getProtectStatusName = (status?: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {

View File

@@ -100,9 +100,10 @@
} }
}, },
{ {
label: '启用实名激活', label: '到期计时基准',
formatter: (_, data) => { formatter: (_, data) => {
return data.enable_realname_activation ? '是' : '' if (!data.expiry_base) return '--'
return data.expiry_base === 'from_activation' ? '实名后开始计时' : '购买即开始计时'
} }
}, },
{ {

View File

@@ -108,6 +108,11 @@
:label="series.series_name" :label="series.series_name"
:value="series.id" :value="series.id"
/> />
<template #empty>
<div style="padding: 10px; text-align: center; color: #909399">
暂无套餐系列请先创建套餐系列
</div>
</template>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
@@ -131,6 +136,38 @@
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="有效期(月)" prop="duration_months">
<ElInputNumber
v-model="form.duration_months"
:min="1"
:max="120"
:controls="false"
style="width: 100%"
placeholder="请输入有效期(月)"
/>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 流量重置周期和套餐周期类型 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="流量重置周期" prop="data_reset_cycle">
<ElSelect
v-model="form.data_reset_cycle"
placeholder="请选择流量重置周期"
style="width: 100%"
clearable
>
<ElOption label="每日" value="daily" />
<ElOption label="每月" value="monthly" />
<ElOption label="每年" value="yearly" />
<ElOption label="不重置" value="none" />
</ElSelect>
</ElFormItem>
</ElCol>
<!-- 只有流量重置周期为每月时才显示套餐周期类型 -->
<ElCol :span="12" v-if="form.data_reset_cycle === 'monthly'">
<ElFormItem label="套餐周期类型" prop="calendar_type"> <ElFormItem label="套餐周期类型" prop="calendar_type">
<ElSelect <ElSelect
v-model="form.calendar_type" v-model="form.calendar_type"
@@ -145,78 +182,24 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElRow :gutter="20"> <!-- 流量重置天数calendar_type为by_day时显示 -->
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="有效期(月)" prop="duration_months"> <ElFormItem label="流量重置天数" prop="duration_days">
<ElInputNumber
v-model="form.duration_months"
:min="1"
:max="120"
:controls="false"
style="width: 100%"
placeholder="请输入有效期(月)"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.calendar_type === 'by_day'">
<ElFormItem label="套餐天数" prop="duration_days">
<ElInputNumber <ElInputNumber
v-model="form.duration_days" v-model="form.duration_days"
:min="1" :min="1"
:max="3650" :max="3650"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入套餐天数" placeholder="请输入流量重置天数"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.calendar_type !== 'by_day'">
<ElFormItem label="流量重置周期" prop="data_reset_cycle">
<ElSelect
v-model="form.data_reset_cycle"
placeholder="请选择流量重置周期"
style="width: 100%"
clearable
>
<ElOption label="每日" value="daily" />
<ElOption label="每月" value="monthly" />
<ElOption label="每年" value="yearly" />
<ElOption label="不重置" value="none" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'">
<ElCol :span="12">
<ElFormItem label="流量重置周期" prop="data_reset_cycle">
<ElSelect
v-model="form.data_reset_cycle"
placeholder="请选择流量重置周期"
style="width: 100%"
clearable
>
<ElOption label="每日" value="daily" />
<ElOption label="每月" value="monthly" />
<ElOption label="每年" value="yearly" />
<ElOption label="不重置" value="none" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="真流量额度(MB)" prop="real_data_mb">
<ElInputNumber
v-model="form.real_data_mb"
:min="0"
:controls="false"
style="width: 100%"
placeholder="请输入真流量额度"
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElRow :gutter="20" v-if="form.calendar_type !== 'by_day'"> <!-- 真流量额度 -->
<ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="真流量额度(MB)" prop="real_data_mb"> <ElFormItem label="真流量额度(MB)" prop="real_data_mb">
<ElInputNumber <ElInputNumber
@@ -239,16 +222,8 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'"> <!-- 虚流量额度和到期计时基准 -->
<ElCol :span="12"> <ElRow :gutter="20">
<ElFormItem label="启用虚流量">
<ElSwitch
v-model="form.enable_virtual_data"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.enable_virtual_data"> <ElCol :span="12" v-if="form.enable_virtual_data">
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb"> <ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb">
<ElInputNumber <ElInputNumber
@@ -260,97 +235,23 @@
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12" v-if="!form.enable_virtual_data">
<ElFormItem label="启用实名激活">
<ElSwitch
v-model="form.enable_realname_activation"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.calendar_type !== 'by_day' && form.enable_virtual_data">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb"> <ElFormItem label="到期计时基准" prop="expiry_base">
<ElInputNumber <ElSelect
v-model="form.virtual_data_mb" v-model="form.expiry_base"
:min="0" placeholder="请选择到期计时基准"
:controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入虚流量额度" clearable
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="启用实名激活">
<ElSwitch
v-model="form.enable_realname_activation"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.calendar_type !== 'by_day' && !form.enable_virtual_data">
<ElCol :span="12">
<ElFormItem label="启用实名激活">
<ElSwitch
v-model="form.enable_realname_activation"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber
v-model="form.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day' && form.enable_virtual_data">
<ElCol :span="12">
<ElFormItem label="启用实名激活">
<ElSwitch
v-model="form.enable_realname_activation"
active-text="启用"
inactive-text="不启用"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber
v-model="form.cost_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow
:gutter="20"
v-if="
(form.calendar_type !== 'by_day' && form.enable_virtual_data) ||
(form.calendar_type === 'by_day' && !form.enable_virtual_data)
"
> >
<ElOption label="实名后开始计时" value="from_activation" />
<ElOption label="购买即开始计时" value="from_purchase" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<!-- 成本价和建议零售价 -->
<ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="成本价(元)" prop="cost_price"> <ElFormItem label="成本价(元)" prop="cost_price">
<ElInputNumber <ElInputNumber
@@ -365,7 +266,7 @@
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="建议售价(元)" prop="suggested_retail_price"> <ElFormItem label="建议售价(元)" prop="suggested_retail_price">
<ElInputNumber <ElInputNumber
v-model="form.suggested_retail_price" v-model="form.suggested_retail_price"
:min="0" :min="0"
@@ -373,23 +274,7 @@
:step="0.01" :step="0.01"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入建议售价(可选)" placeholder="请输入建议售价"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day' && form.enable_virtual_data">
<ElCol :span="12">
<ElFormItem label="建议售价(元)" prop="suggested_retail_price">
<ElInputNumber
v-model="form.suggested_retail_price"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入建议售价(可选)"
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
@@ -690,7 +575,7 @@
duration_months: 1, duration_months: 1,
data_reset_cycle: undefined, data_reset_cycle: undefined,
enable_virtual_data: false, enable_virtual_data: false,
enable_realname_activation: false, expiry_base: 'from_activation',
real_data_mb: 0, real_data_mb: 0,
virtual_data_mb: 0, virtual_data_mb: 0,
cost_price: 0, cost_price: 0,
@@ -870,6 +755,27 @@
} }
) )
// 监听流量重置周期变化,不是每月时清空套餐周期类型
watch(
() => form.data_reset_cycle,
(cycle) => {
if (cycle !== 'monthly') {
form.calendar_type = undefined
form.duration_days = undefined
}
}
)
// 监听套餐周期类型变化,不是按天时清空套餐天数
watch(
() => form.calendar_type,
(type) => {
if (type !== 'by_day') {
form.duration_days = undefined
}
}
)
onMounted(() => { onMounted(() => {
loadSeriesOptions() loadSeriesOptions()
loadSearchSeriesOptions() loadSearchSeriesOptions()
@@ -1008,7 +914,7 @@
form.duration_months = row.duration_months form.duration_months = row.duration_months
form.data_reset_cycle = row.data_reset_cycle || undefined form.data_reset_cycle = row.data_reset_cycle || undefined
form.enable_virtual_data = row.enable_virtual_data || false form.enable_virtual_data = row.enable_virtual_data || false
form.enable_realname_activation = row.enable_realname_activation || false form.expiry_base = row.expiry_base || 'from_activation'
form.real_data_mb = row.real_data_mb || 0 form.real_data_mb = row.real_data_mb || 0
form.virtual_data_mb = row.virtual_data_mb || 0 form.virtual_data_mb = row.virtual_data_mb || 0
form.cost_price = row.cost_price / 100 // 分转换为元显示 form.cost_price = row.cost_price / 100 // 分转换为元显示
@@ -1027,7 +933,7 @@
form.duration_months = 1 form.duration_months = 1
form.data_reset_cycle = undefined form.data_reset_cycle = undefined
form.enable_virtual_data = false form.enable_virtual_data = false
form.enable_realname_activation = false form.expiry_base = 'from_activation'
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.cost_price = 0 form.cost_price = 0
@@ -1066,7 +972,7 @@
form.duration_months = 1 form.duration_months = 1
form.data_reset_cycle = undefined form.data_reset_cycle = undefined
form.enable_virtual_data = false form.enable_virtual_data = false
form.enable_realname_activation = false form.expiry_base = 'from_activation'
form.real_data_mb = 0 form.real_data_mb = 0
form.virtual_data_mb = 0 form.virtual_data_mb = 0
form.cost_price = 0 form.cost_price = 0
@@ -1116,22 +1022,23 @@
duration_months: form.duration_months, duration_months: form.duration_months,
cost_price: costPriceInCents, cost_price: costPriceInCents,
enable_virtual_data: form.enable_virtual_data || false, enable_virtual_data: form.enable_virtual_data || false,
enable_realname_activation: form.enable_realname_activation || false expiry_base: form.expiry_base || 'from_activation'
} }
// 可选字段 // 可选字段
if (form.series_id) { if (form.series_id) {
data.series_id = form.series_id data.series_id = form.series_id
} }
if (form.calendar_type) { if (form.data_reset_cycle) {
data.data_reset_cycle = form.data_reset_cycle
}
// 只有流量重置周期为 monthly 时,才传递 calendar_type
if (form.data_reset_cycle === 'monthly' && form.calendar_type) {
data.calendar_type = form.calendar_type data.calendar_type = form.calendar_type
} }
if (form.calendar_type === 'by_day' && form.duration_days) { if (form.calendar_type === 'by_day' && form.duration_days) {
data.duration_days = form.duration_days data.duration_days = form.duration_days
} }
if (form.data_reset_cycle) {
data.data_reset_cycle = form.data_reset_cycle
}
if (suggestedRetailPriceInCents !== undefined) { if (suggestedRetailPriceInCents !== undefined) {
data.suggested_retail_price = suggestedRetailPriceInCents data.suggested_retail_price = suggestedRetailPriceInCents
} }