fix: bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m9s

This commit is contained in:
sexygoat
2026-04-23 14:59:05 +08:00
parent a9467e8a0c
commit a5e76313cb
28 changed files with 1284 additions and 702 deletions

View File

@@ -194,6 +194,11 @@
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="实名认证策略" width="120">
<template #default="scope">
{{ getRealnamePolicyName(scope.row.realname_policy) }}
</template>
</ElTableColumn>
<ElTableColumn label="实名时间" width="180">
<template #default="scope">
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
@@ -229,7 +234,7 @@
<!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="deviceRealtime" style="margin-top: 16px">
<div v-if="deviceRealtime" v-loading="realtimeLoading" style="margin-top: 16px">
<ElDescriptions :column="4" border>
<!-- 基本状态 -->
<ElDescriptionsItem label="在线状态">
@@ -522,9 +527,12 @@
cardInfo: AssetInfo
deviceRealtime: DeviceRealtimeInfo | null
pollingEnabled: boolean
realtimeLoading?: boolean
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
realtimeLoading: false
})
const { hasAuth } = useAuth()

View File

@@ -2,66 +2,9 @@
<ElCard shadow="never" class="info-card current-package-card">
<template #header>
<div class="card-header">
<div class="card-header-left">
<span class="header-title">当前生效套餐</span>
<ElTag
v-if="currentPackage"
:type="getPackageStatusTagType(currentPackage.status)"
size="small"
>
{{ currentPackage?.status_name || '-' }}
</ElTag>
</div>
<!-- 中间流量进度 -->
<div v-if="currentPackage && !props.boundDeviceId" class="header-progress">
<div class="header-progress-info">
<span class="progress-title">流量</span>
<span class="progress-text">
<template v-if="isAdminOrPlatform">
总量 {{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }} / 实际可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_limit_mb || 0) : (currentPackage.data_limit_mb || currentPackage.package_total_data || 0)) }} / 剩余可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_remain_mb || 0) : ((currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0))) }}
</template>
<template v-else>
总量 {{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }} / 剩余可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_remain_mb || 0) : ((currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0))) }}
</template>
</span>
</div>
<ElProgress
:percentage="
currentPackage.enable_virtual_data
? getUsageProgress(
currentPackage.virtual_data_used || 0,
currentPackage.virtual_data_total || 0
)
: getUsageProgress(
currentPackage.real_data_used || 0,
currentPackage.real_data_total || 0
)
"
:color="
currentPackage.enable_virtual_data
? getProgressColorByPercentage(
getUsageProgress(
currentPackage.virtual_data_used || 0,
currentPackage.virtual_data_total || 0
)
)
: getProgressColorByPercentage(
getUsageProgress(
currentPackage.real_data_used || 0,
currentPackage.real_data_total || 0
)
)
"
:stroke-width="10"
/>
</div>
<div v-if="currentPackage" class="card-header-right">
<ElButton type="primary" size="small" @click="handleShowRecharge"> 套餐充值 </ElButton>
<span class="header-title">当前生效套餐</span>
<div class="header-actions">
<slot name="header-actions" />
</div>
</div>
</template>
@@ -86,33 +29,80 @@
<!-- 显示错误信息 -->
<ElAlert v-else-if="errorMsg" :title="errorMsg" type="warning" :closable="false" />
<!-- 显示套餐详情 -->
<ElDescriptions v-else-if="currentPackage" :column="3" border>
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐价格">
{{ formatAmount(currentPackage.package_price) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem
v-if="currentPackage.enable_virtual_data && isAdminOrPlatform"
label="虚流量比例"
>
{{ currentPackage.virtual_ratio || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长" :span="3">
{{ currentPackage.duration_days }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatDateTime(currentPackage.start_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期时间">
{{ formatDateTime(currentPackage.expire_time) || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
<template v-else-if="currentPackage">
<!-- 流量使用情况 - 横向进度条卡片 -->
<div class="flow-progress-card">
<div class="flow-stats-row">
<div class="flow-stat-item">
<span class="flow-stat-label">总量</span>
<span class="flow-stat-value">
{{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }}
</span>
</div>
<div v-if="isAdminOrPlatform && currentPackage.enable_virtual_data" class="flow-stat-item">
<span class="flow-stat-label">实际可用</span>
<span class="flow-stat-value">
{{ formatDataSize(currentPackage.virtual_limit_mb || 0) }}
</span>
</div>
<div class="flow-stat-item">
<span class="flow-stat-label">剩余可用</span>
<span class="flow-stat-value">
{{
formatDataSize(
currentPackage.enable_virtual_data
? currentPackage.virtual_remain_mb || 0
: (currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0)
)
}}
</span>
</div>
</div>
<div class="flow-progress-bar">
<ElProgress
:percentage="
currentPackage.enable_virtual_data
? getUsageProgress(currentPackage.virtual_data_used || 0, currentPackage.virtual_data_total || 0)
: getUsageProgress(currentPackage.real_data_used || 0, currentPackage.real_data_total || 0)
"
:color="
currentPackage.enable_virtual_data
? getProgressColorByPercentage(getUsageProgress(currentPackage.virtual_data_used || 0, currentPackage.virtual_data_total || 0))
: getProgressColorByPercentage(getUsageProgress(currentPackage.real_data_used || 0, currentPackage.real_data_total || 0))
"
:stroke-width="14"
/>
</div>
</div>
<!-- 套餐详情表格 -->
<ElDescriptions :column="3" border class="package-info-table">
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐价格">
{{ formatAmount(currentPackage.paid_amount || currentPackage.package_price || 0) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="isAdminOrPlatform && currentPackage.enable_virtual_data" label="虚流量比例">
{{ currentPackage.virtual_ratio || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatDateTime(currentPackage.start_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期时间">
{{ formatDateTime(currentPackage.expire_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="购买时间">
{{ formatDateTime(currentPackage.created_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长">
{{ currentPackage.duration_days }}
</ElDescriptionsItem>
</ElDescriptions>
</template>
<!-- 空状态 -->
<ElEmpty v-else-if="!loading" :description="errorMsg || '暂无当前生效套餐'" :image-size="100" />
@@ -122,7 +112,6 @@
<script setup lang="ts">
import {
ElCard,
ElTag,
ElButton,
ElDescriptions,
ElDescriptionsItem,
@@ -136,21 +125,19 @@
import { formatDateTime } from '@/utils/business/format'
import type { PackageInfo } from '../types'
// Use formatters
const {
formatAmount,
formatDataSize,
getPackageStatusTagType,
getUsageProgress,
getProgressColorByPercentage
} = useAssetFormatters()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isSuperAdmin = computed(() => userInfo.value.user_type === 1)
const isAdminOrPlatform = computed(() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2)
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
// Props
interface Props {
currentPackage: PackageInfo | null
loading?: boolean
@@ -168,18 +155,11 @@
boundDeviceName: ''
})
// Emits
const emit = defineEmits<{
showRecharge: []
navigateToDevice: [deviceNo: string]
}>()
// Methods
const handleShowRecharge = () => {
emit('showRecharge')
}
// 套餐类型名称
const getPackageTypeName = (type?: string) => {
const typeMap: Record<string, string> = {
formal: '正式套餐',
@@ -195,52 +175,49 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.card-header-left {
display: flex;
gap: 12px;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 500;
}
}
.card-header-right {
display: flex;
gap: 8px;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 500;
}
}
.header-progress {
flex: 1;
min-width: 0;
padding: 0 24px;
.flow-progress-card {
padding: 20px;
background: var(--el-fill-color-light);
border-radius: 8px;
margin-bottom: 20px;
.header-progress-info {
.flow-stats-row {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 6px;
gap: 32px;
margin-bottom: 16px;
.progress-title {
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.flow-stat-item {
display: flex;
align-items: baseline;
gap: 8px;
.progress-text {
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.flow-stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.flow-stat-value {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.flow-progress-bar {
padding: 0 4px;
}
}
.package-info-table {
margin-top: 0;
}
}
</style>

View File

@@ -3,9 +3,12 @@
<template #header>
<div class="card-header">
<span>套餐列表</span>
<div class="card-header-right">
<ElButton type="primary" @click="handleShowRecharge">套餐充值</ElButton>
</div>
</div>
</template>
<div class="package-table-wrapper">
<div v-loading="loading" class="package-table-wrapper">
<!-- 绑定设备提示 -->
<ElEmpty v-if="props.boundDeviceId" :image-size="100">
<template #description>
@@ -36,7 +39,7 @@
</ElTableColumn>
<ElTableColumn label="套餐价格" width="120">
<template #default="scope">
{{ formatAmount(scope.row.package_price || 0) }}
{{ formatAmount(scope.row.paid_amount || scope.row.package_price || 0) }}
</template>
</ElTableColumn>
<ElTableColumn prop="status_name" label="套餐状态" width="100">
@@ -104,6 +107,11 @@
{{ formatDateTime(scope.row.expires_at) }}
</template>
</ElTableColumn>
<ElTableColumn prop="created_at" label="创建时间" width="170" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
<ElPagination
v-model:current-page="pagination.page"
@@ -152,6 +160,7 @@
pageSize?: number
boundDeviceId?: number
boundDeviceNo?: string
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -159,12 +168,14 @@
page: 1,
pageSize: 10,
boundDeviceId: undefined,
boundDeviceNo: ''
boundDeviceNo: '',
loading: false
})
// Emits
interface Emits {
(e: 'showDailyRecords', payload: { packageRow: PackageInfo }): void
(e: 'showRecharge'): void
(e: 'page-change', page: number): void
(e: 'size-change', size: number): void
(e: 'navigateToDevice', deviceNo: string): void
@@ -212,6 +223,10 @@
emit('showDailyRecords', { packageRow })
}
const handleShowRecharge = () => {
emit('showRecharge')
}
// 页码变化
const handlePageChange = (page: number) => {
emit('page-change', page)
@@ -229,6 +244,13 @@
width: 100%;
overflow: hidden; // 防止内容溢出
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.package-table-wrapper {
.table-scroll-container {
overflow-x: auto;

View File

@@ -1,5 +1,5 @@
<template>
<ElCard shadow="never" class="info-card wallet-info">
<ElCard shadow="never" class="info-card wallet-info" v-loading="walletLoading">
<template #header>
<div class="card-header wallet-header">
<div class="header-left">

View File

@@ -29,6 +29,8 @@ export function useAssetInfo() {
const packageList = ref<AssetPackageUsageRecord[]>([])
const walletInfo = ref<AssetWalletResponse | null>(null)
const walletLoading = ref(false)
const packageLoading = ref(false)
const realtimeLoading = ref(false)
/**
* 获取资产详情
@@ -142,6 +144,7 @@ export function useAssetInfo() {
*/
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10) => {
try {
packageLoading.value = true
const response = await AssetService.getAssetPackages(identifier, {
page,
page_size: pageSize
@@ -161,6 +164,8 @@ export function useAssetInfo() {
} catch (error) {
console.error('获取套餐列表失败:', error)
return { items: [], total: 0, page: 1, pageSize }
} finally {
packageLoading.value = false
}
}
@@ -173,23 +178,17 @@ export function useAssetInfo() {
currentPackageLoading.value = true
currentPackageErrorMsg.value = ''
const [currentPkgResponse, packageListResponse] = await Promise.all([
AssetService.getCurrentPackage(identifier),
AssetService.getAssetPackages(identifier, { page: 1, page_size: 50 })
])
const response = await AssetService.getCurrentPackage(identifier)
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
if (response.code === 0) {
if (response.data) {
const pkg = response.data
currentPackage.value = {
id: pkg.package_usage_id || 0,
package_id: pkg.package_id || 0,
package_name: pkg.package_name || '',
package_price: activePaidAmount,
paid_amount: pkg.paid_amount || 0,
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),
@@ -208,7 +207,8 @@ export function useAssetInfo() {
data_limit_mb: pkg.data_limit_mb,
virtual_limit_mb: pkg.virtual_limit_mb,
virtual_remain_mb: pkg.virtual_remain_mb,
data_usage_mb: pkg.data_usage_mb
data_usage_mb: pkg.data_usage_mb,
created_at: pkg.created_at || ''
}
// 同时更新 cardInfo 中的流量显示信息(根据是否启用虚流量选择数据源)
@@ -260,6 +260,7 @@ export function useAssetInfo() {
*/
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
try {
realtimeLoading.value = true
const response = await AssetService.getRealtimeStatus(identifier)
if (response.code === 0 && response.data) {
const data = response.data
@@ -323,6 +324,8 @@ export function useAssetInfo() {
}
} catch (error) {
console.error('获取实时状态失败:', error)
} finally {
realtimeLoading.value = false
}
}
@@ -377,8 +380,10 @@ export function useAssetInfo() {
currentPackageLoading,
currentPackageErrorMsg,
packageList,
packageLoading,
walletInfo,
walletLoading,
realtimeLoading,
// Methods
fetchAssetDetail,

View File

@@ -17,6 +17,7 @@
:card-info="cardInfo"
:device-realtime="deviceRealtime"
v-model:polling-enabled="pollingEnabled"
:realtime-loading="realtimeLoading"
@enable-card="handleEnableCard"
@disable-card="handleDisableCard"
@manual-deactivate="handleManualDeactivate"
@@ -56,7 +57,9 @@
:page-size="packagePagination.pageSize"
:bound-device-id="cardInfo.bound_device_id"
:bound-device-no="cardInfo.bound_device_no"
:loading="packageLoading"
@show-daily-records="handleShowDailyRecords"
@show-recharge="showRechargeDialog"
@page-change="handlePackagePageChange"
@size-change="handlePackageSizeChange"
@navigate-to-device="handleNavigateToDevice"
@@ -172,8 +175,10 @@
currentPackageLoading,
currentPackageErrorMsg,
packageList,
packageLoading,
walletInfo,
walletLoading,
realtimeLoading,
fetchAssetDetail,
loadPackageList,
loadCurrentPackage,

View File

@@ -5,6 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="100"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -340,7 +341,11 @@
<ElRow :gutter="20">
<ElCol :span="6">
<ElFormItem label="搜索类型" prop="search_type">
<ElSelect v-model="bindCardForm.search_type" placeholder="请选择搜索类型" style="width: 100%">
<ElSelect
v-model="bindCardForm.search_type"
placeholder="请选择搜索类型"
style="width: 100%"
>
<ElOption label="ICCID" value="iccid" />
<ElOption label="运营商名称" value="carrier_name" />
</ElSelect>
@@ -655,6 +660,10 @@
})
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
// 搜索表单店铺/系列选项
const shopOptions = ref<any[]>([])
const searchSeriesOptions = ref<any[]>([])
// 设备详情弹窗相关
const deviceDetailDialogVisible = ref(false)
const deviceDetailLoading = ref(false)
@@ -734,7 +743,12 @@
status: undefined as DeviceStatus | undefined,
batch_no: '',
device_type: '',
manufacturer: ''
manufacturer: '',
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
// 搜索表单
@@ -801,6 +815,54 @@
clearable: true,
placeholder: '请输入制造商'
}
},
{
label: '店铺名称',
prop: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query),
loading: false,
placeholder: '请选择或搜索店铺名称'
},
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchSeries(query),
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '起始至结束',
prop: 'dateRange',
type: 'date',
config: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZZ'
}
}
]
@@ -814,21 +876,26 @@
// 列配置
const columnOptions = [
{ label: '设备号', prop: 'virtual_no' },
{ label: 'IMEI', prop: 'imei' },
{ label: '设备名称', prop: 'device_name' },
{ label: '设备型号', prop: 'device_model' },
{ label: '设备类型', prop: 'device_type' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '设备型号', prop: 'device_model' },
{ label: '设备类型', prop: 'device_type' },
{ label: '状态', prop: 'status' },
{ label: '状态名称', prop: 'status_name' },
{ label: '在线状态', prop: 'online_status' },
{ label: '实名策略', prop: 'realname_policy' },
{ label: '切卡模式', prop: 'switch_mode' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '制造商', prop: 'manufacturer' },
{ label: '软件版本', prop: 'software_version' },
{ label: '最大插槽数', prop: 'max_sim_slots' },
{ label: '已绑定卡数', prop: 'bound_card_count' },
{ label: '状态', prop: 'status' },
{ label: '在线状态', prop: 'online_status' },
{ label: '切卡模式', prop: 'switch_mode' },
{ label: '序列号', prop: 'sn' },
{ label: '批次号', prop: 'batch_no' },
{ label: '最后在线时间', prop: 'last_online_time' },
{ label: '最后同步时间', prop: 'last_gateway_sync_at' },
{ label: '批次号', prop: 'batch_no' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
@@ -875,10 +942,7 @@
bindCardForm.iot_card_id = undefined
bindCardForm.slot_position = 1
// 加载未绑定设备的卡列表
await Promise.all([
loadDeviceCards(device.virtual_no),
loadDefaultIotCards()
])
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
}
// 加载设备绑定的卡列表
@@ -1055,7 +1119,7 @@
{
prop: 'virtual_no',
label: '设备号',
minWidth: 150,
minWidth: 170,
showOverflowTooltip: true,
formatter: (row: Device) => {
return h(
@@ -1071,21 +1135,18 @@
)
}
},
{
prop: 'imei',
label: 'IMEI',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: Device) => row.imei || '-'
},
{
prop: 'device_name',
label: '设备名称',
minWidth: 120
},
{
prop: 'device_model',
label: '设备型号',
minWidth: 120
},
{
prop: 'device_type',
label: '设备类型',
width: 120
},
{
prop: 'shop_name',
label: '店铺名称',
@@ -1097,23 +1158,14 @@
minWidth: 120
},
{
prop: 'manufacturer',
label: '制造商',
prop: 'device_model',
label: '设备型号',
minWidth: 120
},
{
prop: 'max_sim_slots',
label: '最大插槽数',
width: 100
},
{
prop: 'bound_card_count',
label: '已绑定卡数',
width: 110,
formatter: (row: Device) => {
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
}
prop: 'device_type',
label: '设备类型',
width: 120
},
{
prop: 'status',
@@ -1130,6 +1182,12 @@
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
prop: 'status_name',
label: '状态名称',
width: 100,
formatter: (row: Device) => row.status_name || '-'
},
{
prop: 'online_status',
label: '在线状态',
@@ -1144,6 +1202,20 @@
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
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: 'switch_mode',
label: '切卡模式',
@@ -1157,19 +1229,51 @@
}
},
{
prop: 'realname_policy',
label: '实名认证策略',
width: 150,
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: Device) => (row.activated_at ? formatDateTime(row.activated_at) : '-')
},
{
prop: 'manufacturer',
label: '制造商',
minWidth: 100
},
{
prop: 'software_version',
label: '软件版本',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Device) => row.software_version || '-'
},
{
prop: 'max_sim_slots',
label: '最大插槽数',
width: 100
},
{
prop: 'bound_card_count',
label: '已绑定卡数',
width: 110,
formatter: (row: Device) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
return policyMap[policy] || (policy ? policy : '未知')
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
}
},
{
prop: 'sn',
label: '序列号',
minWidth: 120,
showOverflowTooltip: true,
formatter: (row: Device) => row.sn || '-'
},
{
prop: 'batch_no',
label: '批次号',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Device) => row.batch_no || '-'
},
{
prop: 'last_online_time',
label: '最后在线时间',
@@ -1184,12 +1288,6 @@
formatter: (row: Device) =>
row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-'
},
{
prop: 'batch_no',
label: '批次号',
minWidth: 180,
formatter: (row: Device) => row.batch_no || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -1198,9 +1296,50 @@
}
])
// 搜索店铺(用于搜索表单)
const searchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 搜索套餐系列(用于搜索表单)
const searchSeries = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20,
status: 1
}
if (query) {
params.series_name = query
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索套餐系列失败:', error)
}
}
let isFirstActivation = true
onMounted(() => {
getTableData()
searchShops('')
searchSeries('')
})
// 当页面被 keep-alive 激活时自动刷新数据
@@ -1223,7 +1362,11 @@
status: searchForm.status,
batch_no: searchForm.batch_no || undefined,
device_type: searchForm.device_type || undefined,
manufacturer: searchForm.manufacturer || undefined
manufacturer: searchForm.manufacturer || undefined,
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
const res = await DeviceService.getDevices(params)
if (res.code === 0 && res.data) {
@@ -1246,6 +1389,14 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.created_at_start = searchForm.dateRange[0] || ''
searchForm.created_at_end = searchForm.dateRange[1] || ''
} else {
searchForm.created_at_start = ''
searchForm.created_at_end = ''
}
pagination.page = 1
getTableData()
}

View File

@@ -229,6 +229,7 @@
const searchForm = reactive({
status: undefined,
identifier: '',
created_at_range: [],
created_at_start: '',
created_at_end: ''
})
@@ -306,11 +307,11 @@
prop: 'created_at_range',
type: 'date',
config: {
type: 'datetimerange',
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
@@ -417,12 +418,23 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.created_at_range && Array.isArray(searchForm.created_at_range)) {
searchForm.created_at_start = searchForm.created_at_range[0] || ''
searchForm.created_at_end = searchForm.created_at_range[1] || ''
} else {
searchForm.created_at_start = ''
searchForm.created_at_end = ''
}
pagination.page = 1
loadExchangeList()
}
// 重置
const handleReset = () => {
searchForm.created_at_range = []
searchForm.created_at_start = ''
searchForm.created_at_end = ''
pagination.page = 1
loadExchangeList()
}

View File

@@ -562,7 +562,13 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import { CardService, ShopService, PackageSeriesService, AssetService, CarrierService } 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'
@@ -659,6 +665,29 @@
}
}
// 套餐系列搜索相关
const searchSeriesOptions = ref<any[]>([])
// 搜索套餐系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20,
status: 1
}
if (query) {
params.series_name = query
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索套餐系列失败:', error)
}
}
// 卡详情弹窗相关
const cardDetailDialogVisible = ref(false)
const cardDetailLoading = ref(false)
@@ -767,6 +796,7 @@
msisdn: string
virtual_no: string
device_virtual_no: string
series_id: undefined | number
is_distributed: undefined | number
is_standalone: undefined | boolean
[key: string]: any
@@ -777,6 +807,7 @@
msisdn: '',
virtual_no: '',
device_virtual_no: '',
series_id: undefined,
is_distributed: undefined,
is_standalone: undefined,
is_replaced: undefined,
@@ -952,6 +983,23 @@
placeholder: '请输入绑定设备虚拟号'
}
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '是否已分销',
prop: 'is_distributed',
@@ -974,8 +1022,8 @@
placeholder: '全部'
},
options: () => [
{ label: '是(未绑定设备', value: true },
{ label: '否(已绑定设备', value: false }
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
},
{
@@ -1016,18 +1064,28 @@
{ label: 'ICCID', prop: 'iccid' },
{ label: 'MSISDN', prop: 'msisdn' },
{ label: '卡虚拟号', prop: 'virtual_no' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '状态', prop: 'status' },
{ label: '激活状态', prop: 'activation_status' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '实名认证策略', prop: 'realname_policy' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '自然月累计流量(MB)', prop: 'current_month_usage_mb' },
{ label: '本月开始日期', prop: 'current_month_start_date' },
{ label: '上月流量总量(MB)', prop: 'last_month_total_mb' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '创建时间', prop: 'created_at' }
{ label: '最后流量检查时间', prop: 'last_data_check_at' },
{ label: '最后实名检查时间', prop: 'last_real_name_check_at' },
{ label: '启用轮询', prop: 'enable_polling' },
{ label: '批次号', prop: 'batch_no' },
{ label: '供应商', prop: 'supplier' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
const cardList = ref<StandaloneIotCard[]>([])
@@ -1233,12 +1291,70 @@
return text
}
},
{
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.activated_at ? formatDateTime(row.activated_at) : '-'
},
{
prop: 'current_month_usage_mb',
label: '自然月累计流量(MB)',
width: 180,
formatter: (row: StandaloneIotCard) => row.current_month_usage_mb?.toLocaleString() || '0'
},
{
prop: 'current_month_start_date',
label: '本月开始日期',
width: 200,
formatter: (row: StandaloneIotCard) =>
row.current_month_start_date ? formatDateTime(row.current_month_start_date) : '-'
},
{
prop: 'last_month_total_mb',
label: '上月流量总量(MB)',
width: 150,
formatter: (row: StandaloneIotCard) => row.last_month_total_mb?.toLocaleString() || '0'
},
{
prop: 'data_usage_mb',
label: '累计流量(MB)',
width: 120,
formatter: (row: StandaloneIotCard) => row.data_usage_mb?.toLocaleString() || '0'
},
{
prop: 'last_data_check_at',
label: '最后流量检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_data_check_at ? formatDateTime(row.last_data_check_at) : '-'
},
{
prop: 'last_real_name_check_at',
label: '最后实名检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_real_name_check_at ? formatDateTime(row.last_real_name_check_at) : '-'
},
{
prop: 'enable_polling',
label: '启用轮询',
width: 100,
formatter: (row: StandaloneIotCard) => (row.enable_polling ? '是' : '否')
},
{
prop: 'batch_no',
label: '批次号',
width: 200,
showOverflowTooltip: true
},
{
prop: 'supplier',
label: '供应商',
width: 150,
formatter: (row: StandaloneIotCard) => row.supplier || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -1289,7 +1405,6 @@
}
onMounted(() => {
loadSearchCarrierOptions()
getTableData()
})

View File

@@ -59,7 +59,27 @@
},
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '来源所有者', prop: 'from_owner_name' },
{
label: '来源类型',
formatter: (_, data) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[data.from_owner_type] || data.from_owner_type || '-'
}
},
{ label: '目标所有者', prop: 'to_owner_name' },
{
label: '目标类型',
formatter: (_, data) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[data.to_owner_type] || data.to_owner_type || '-'
}
},
{ label: '操作人', prop: 'operator_name' },
{ label: '关联卡数量', prop: 'related_card_count' },
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },

View File

@@ -28,28 +28,13 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -58,7 +43,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService } from '@/api/modules'
import { CardService, ShopService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -67,28 +52,14 @@
import { formatDateTime } from '@/utils/business/format'
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
import { RoutesAlias } from '@/router/routesAlias'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
defineOptions({ name: 'AssetAllocationRecords' })
const { hasAuth } = useAuth()
const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const tableRef = ref()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<AssetAllocationRecord | null>(null)
// 搜索表单初始值
const initialSearchState = {
@@ -99,6 +70,7 @@
from_shop_id: undefined as number | undefined,
to_shop_id: undefined as number | undefined,
operator_id: undefined as number | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
@@ -159,16 +131,51 @@
placeholder: 'ICCID或设备号'
}
},
{
label: '来源店铺',
prop: 'from_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query, 'from'),
placeholder: '请选择或搜索来源店铺'
},
options: () =>
fromShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '目标店铺',
prop: 'to_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query, 'to'),
placeholder: '请选择或搜索目标店铺'
},
options: () =>
toShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '创建时间',
prop: 'created_at_range',
prop: 'dateRange',
type: 'date',
config: {
type: 'datetimerange',
type: 'daterange',
clearable: true,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
rangeSeparator: '',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
@@ -186,6 +193,33 @@
{ label: '创建时间', prop: 'created_at' }
]
// 店铺搜索选项
const fromShopOptions = ref<any[]>([])
const toShopOptions = ref<any[]>([])
// 搜索店铺
const searchShops = async (query: string, type: 'from' | 'to') => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
if (type === 'from') {
fromShopOptions.value = res.data.items || []
} else {
toShopOptions.value = res.data.items || []
}
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
const recordList = ref<AssetAllocationRecord[]>([])
// 获取分配类型标签类型
@@ -246,12 +280,38 @@
{
prop: 'from_owner_name',
label: '来源所有者',
width: 150
width: 150,
formatter: (row: AssetAllocationRecord) => row.from_owner_name || '-'
},
{
prop: 'from_owner_type',
label: '来源类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[row.from_owner_type] || row.from_owner_type || '-'
}
},
{
prop: 'to_owner_name',
label: '目标所有者',
width: 150
width: 150,
formatter: (row: AssetAllocationRecord) => row.to_owner_name || '-'
},
{
prop: 'to_owner_type',
label: '目标类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[row.to_owner_type] || row.to_owner_type || '-'
}
},
{
prop: 'operator_name',
@@ -279,6 +339,8 @@
onMounted(() => {
getTableData()
searchShops('', 'from')
searchShops('', 'to')
})
// 获取分配记录列表
@@ -291,13 +353,6 @@
...formFilters
}
// 处理日期范围
if ((params as any).created_at_range && (params as any).created_at_range.length === 2) {
params.created_at_start = (params as any).created_at_range[0]
params.created_at_end = (params as any).created_at_range[1]
delete (params as any).created_at_range
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
@@ -327,6 +382,14 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (formFilters.dateRange && Array.isArray(formFilters.dateRange)) {
formFilters.created_at_start = formFilters.dateRange[0] || ''
formFilters.created_at_end = formFilters.dateRange[1] || ''
} else {
formFilters.created_at_start = ''
formFilters.created_at_end = ''
}
pagination.page = 1
getTableData()
}
@@ -362,40 +425,10 @@
ElMessage.warning('您没有查看详情的权限')
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: AssetAllocationRecord, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (
item.key
// No cases available
) {
}
}
</script>
<style lang="scss" scoped>
.asset-allocation-records-page {
// Allocation records page styles
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -82,7 +82,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AuthorizationService } from '@/api/modules'
import { AuthorizationService, EnterpriseService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
@@ -125,6 +125,28 @@
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<AuthorizationItem | null>(null)
// 企业搜索选项
const enterpriseOptions = ref<any[]>([])
// 搜索企业
const searchEnterprises = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.enterprise_name = query
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
}
}
// 搜索表单初始值
const initialSearchState = {
enterprise_id: undefined as number | undefined,
@@ -165,6 +187,23 @@
placeholder: '请输入ICCID'
}
},
{
label: '企业名称',
prop: 'enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: searchEnterprises,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((e) => ({
label: e.enterprise_name,
value: e.id
}))
},
{
label: '授权人类型',
prop: 'authorizer_type',
@@ -309,6 +348,7 @@
onMounted(() => {
getTableData()
searchEnterprises('')
})
// 获取授权记录列表