弹窗改为跳转链接
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m41s

This commit is contained in:
sexygoat
2026-03-07 11:41:15 +08:00
parent 8fbc321a5e
commit e73992d253
11 changed files with 1238 additions and 245 deletions

View File

@@ -54,6 +54,14 @@ export class DeviceService extends BaseService {
return this.getOne<Device>(`/api/admin/devices/by-imei/${imei}`)
}
/**
* 通过ICCID查询设备详情
* @param iccid ICCID
*/
static getDeviceByIccid(iccid: string): Promise<BaseResponse<any>> {
return this.getOne<any>(`/api/admin/devices/by-iccid/${iccid}`)
}
/**
* 删除设备
* @param id 设备ID

View File

@@ -444,6 +444,7 @@
"deviceSearch": "设备查询",
"singleCard": "单卡信息",
"standaloneCardList": "IoT卡管理",
"iotCardDetail": "IoT卡详情",
"iotCardTask": "IoT卡任务",
"deviceTask": "设备任务",
"taskDetail": "任务详情",

View File

@@ -977,6 +977,16 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true
}
},
{
path: 'iot-card-management/detail',
name: 'IotCardDetail',
component: RoutesAlias.StandaloneCardList + '/detail',
meta: {
title: 'menus.assetManagement.iotCardDetail',
isHide: true,
keepAlive: false
}
},
{
path: 'iot-card-task',
name: 'IotCardTask',

View File

@@ -109,6 +109,7 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']

View File

@@ -1,94 +1,78 @@
<template>
<div class="device-detail-page">
<ElPageHeader @back="handleBack" title="设备详情" />
<ElCard shadow="never" style="margin-top: 20px" v-loading="loading">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: bold">基本信息</span>
<ElTag :type="statusTypeMap[deviceInfo?.status || 1]">
{{ deviceInfo?.status_name || '-' }}
</ElTag>
</div>
</template>
<ElDescriptions :column="3" border v-if="deviceInfo">
<ElDescriptionsItem label="设备ID">{{ deviceInfo.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备号">{{ deviceInfo.device_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备名称">{{ deviceInfo.device_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备型号">{{ deviceInfo.device_model }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备类型">{{ deviceInfo.device_type }}</ElDescriptionsItem>
<ElDescriptionsItem label="制造商">{{ deviceInfo.manufacturer }}</ElDescriptionsItem>
<ElDescriptionsItem label="最大插槽数">{{ deviceInfo.max_sim_slots }}</ElDescriptionsItem>
<ElDescriptionsItem label="已绑定卡数">
<span style="font-weight: bold; color: #67c23a">{{ deviceInfo.bound_card_count }}</span>
/ {{ deviceInfo.max_sim_slots }}
</ElDescriptionsItem>
<ElDescriptionsItem label="所属店铺">
{{ deviceInfo.shop_name || '平台库存' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">
{{ deviceInfo.batch_no || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="激活时间">
{{ deviceInfo.activated_at ? formatDateTime(deviceInfo.activated_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(deviceInfo.created_at) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<ElCard shadow="never" style="margin-top: 20px" v-loading="cardsLoading">
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-weight: bold">绑定的卡列表</span>
<ElButton
type="primary"
size="small"
@click="showBindDialog"
:disabled="!deviceInfo || deviceInfo.bound_card_count >= deviceInfo.max_sim_slots"
>
绑定新卡
</ElButton>
</div>
</template>
<ElTable :data="cardList" border>
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center">
<template #default="{ row }">
<ElTag type="info" size="small">插槽 {{ row.slot_position }}</ElTag>
<div class="device-detail">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
</ElTableColumn>
<ElTableColumn prop="iccid" label="ICCID" minWidth="180" />
<ElTableColumn prop="msisdn" label="接入号" width="150">
<template #default="{ row }">
{{ row.msisdn || '-' }}
</template>
</ElTableColumn>
<ElTableColumn prop="carrier_name" label="运营商" width="120" />
<ElTableColumn prop="status" label="卡状态" width="100">
<template #default="{ row }">
<ElTag :type="cardStatusTypeMap[row.status]" size="small">
{{ cardStatusTextMap[row.status] || '未知' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
<template #default="{ row }">
{{ row.bind_time ? formatDateTime(row.bind_time) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<ElButton type="danger" text size="small" @click="handleUnbindCard(row)">
解绑
返回
</ElButton>
<h2 class="detail-title">设备详情</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
<!-- 绑定卡片列表 -->
<ElCard v-if="detailData" shadow="never" class="cards-section" style="margin-top: 20px">
<template #header>
<div class="section-header">
<span class="section-title">绑定的卡列表</span>
<ElButton
type="primary"
size="small"
@click="showBindDialog"
:disabled="!detailData || detailData.bound_card_count >= detailData.max_sim_slots"
>
绑定新卡
</ElButton>
</template>
</ElTableColumn>
</ElTable>
</div>
</template>
<ElEmpty v-if="!cardList.length" description="暂无绑定的卡" />
<ElTable :data="cardList" border v-loading="cardsLoading">
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center">
<template #default="{ row }">
<ElTag type="info" size="small">插槽 {{ row.slot_position }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="iccid" label="ICCID" minWidth="180" />
<ElTableColumn prop="msisdn" label="接入号" width="150">
<template #default="{ row }">
{{ row.msisdn || '-' }}
</template>
</ElTableColumn>
<ElTableColumn prop="carrier_name" label="运营商" width="120" />
<ElTableColumn prop="status" label="卡状态" width="100">
<template #default="{ row }">
<ElTag :type="cardStatusTypeMap[row.status]" size="small">
{{ cardStatusTextMap[row.status] || '未知' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
<template #default="{ row }">
{{ row.bind_time ? formatDateTime(row.bind_time) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<ElButton type="danger" text size="small" @click="handleUnbindCard(row)">
解绑
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-if="!cardList.length && !cardsLoading" description="暂无绑定的卡" />
</ElCard>
</ElCard>
<!-- 绑定卡对话框 -->
@@ -145,24 +129,29 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElMessageBox, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { DeviceService, CardService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { Device, DeviceCardBinding } from '@/types/api'
import type { FormInstance, FormRules } from 'element-plus'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'DeviceDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const cardsLoading = ref(false)
const bindLoading = ref(false)
const searchCardsLoading = ref(false)
const bindDialogVisible = ref(false)
const bindFormRef = ref<FormInstance>()
const deviceInfo = ref<Device | null>(null)
const detailData = ref<Device | null>(null)
const cardList = ref<DeviceCardBinding[]>([])
const availableCards = ref<any[]>([])
@@ -204,34 +193,88 @@
// 可用插槽
const availableSlots = computed(() => {
if (!deviceInfo.value) return []
if (!detailData.value) return []
const occupiedSlots = cardList.value.map((card) => card.slot_position)
const allSlots = Array.from({ length: deviceInfo.value.max_sim_slots }, (_, i) => i + 1)
const allSlots = Array.from({ length: detailData.value.max_sim_slots }, (_, i) => i + 1)
return allSlots.filter((slot) => !occupiedSlots.includes(slot))
})
onMounted(() => {
const deviceId = route.query.id
if (deviceId) {
loadDeviceInfo(Number(deviceId))
loadDeviceCards(Number(deviceId))
} else {
ElMessage.error('设备ID不存在')
handleBack()
// 详情页配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: '设备ID', prop: 'id' },
{ label: '设备号', prop: 'device_no' },
{ label: '设备名称', prop: 'device_name', formatter: (value) => value || '-' },
{ label: '设备型号', prop: 'device_model', formatter: (value) => value || '-' },
{ label: '设备类型', prop: 'device_type', formatter: (value) => value || '-' },
{ label: '制造商', prop: 'manufacturer', formatter: (value) => value || '-' },
{ label: '最大插槽数', prop: 'max_sim_slots' },
{
label: '已绑定卡数',
render: (data: Device) => {
const color = data.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } },
`${data.bound_card_count} / ${data.max_sim_slots}`)
}
},
{
label: '状态',
render: (data: Device) => {
const statusMap: Record<number, { text: string; type: any }> = {
1: { text: '在库', type: 'info' },
2: { text: '已分销', type: 'primary' },
3: { text: '已激活', type: 'success' },
4: { text: '已停用', type: 'danger' }
}
const status = statusMap[data.status] || { text: '未知', type: 'info' }
return h(ElTag, { type: status.type }, () => status.text)
}
},
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '平台库存' },
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
{
label: '激活时间',
prop: 'activated_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) }
]
}
})
]
// 加载设备信息
const loadDeviceInfo = async (id: number) => {
// 返回上一页
const handleBack = () => {
router.back()
}
// 获取详情数据
const fetchDetail = async (id?: number, deviceNo?: string) => {
loading.value = true
try {
const res = await DeviceService.getDeviceById(id)
if (res.code === 0) {
deviceInfo.value = res.data
let res
if (id) {
res = await DeviceService.getDeviceById(id)
} else if (deviceNo) {
res = await DeviceService.getDeviceByImei(deviceNo)
} else {
ElMessage.error('缺少设备参数')
return
}
if (res.code === 0 && res.data) {
detailData.value = res.data
// 加载绑定的卡列表
if (res.data.id) {
loadDeviceCards(res.data.id)
}
} else {
ElMessage.error(res.message || '获取设备详情失败')
}
} catch (error) {
console.error(error)
ElMessage.error('获取设备信息失败')
ElMessage.error('获取设备详情失败')
} finally {
loading.value = false
}
@@ -267,7 +310,7 @@
page_size: 20
})
if (res.code === 0) {
availableCards.value = res.data.list || []
availableCards.value = res.data.items || []
}
} catch (error) {
console.error(error)
@@ -286,7 +329,7 @@
// 确认绑定
const handleConfirmBind = async () => {
if (!bindFormRef.value || !deviceInfo.value) return
if (!bindFormRef.value || !detailData.value) return
await bindFormRef.value.validate(async (valid) => {
if (valid) {
@@ -296,15 +339,16 @@
iot_card_id: bindForm.iot_card_id!,
slot_position: bindForm.slot_position!
}
const res = await DeviceService.bindCard(deviceInfo.value!.id, data)
const res = await DeviceService.bindCard(detailData.value!.id, data)
if (res.code === 0) {
ElMessage.success('绑定成功')
bindDialogVisible.value = false
await loadDeviceInfo(deviceInfo.value!.id)
await loadDeviceCards(deviceInfo.value!.id)
await fetchDetail(detailData.value!.id)
if (bindFormRef.value) {
bindFormRef.value.resetFields()
}
} else {
ElMessage.error(res.message || '绑定失败')
}
} catch (error) {
console.error(error)
@@ -325,10 +369,9 @@
})
.then(async () => {
try {
await DeviceService.unbindCard(deviceInfo.value!.id, card.iot_card_id)
await DeviceService.unbindCard(detailData.value!.id, card.iot_card_id)
ElMessage.success('解绑成功')
await loadDeviceInfo(deviceInfo.value!.id)
await loadDeviceCards(deviceInfo.value!.id)
await fetchDetail(detailData.value!.id)
} catch (error) {
console.error(error)
ElMessage.error('解绑失败')
@@ -339,14 +382,65 @@
})
}
// 返回
const handleBack = () => {
router.back()
}
onMounted(() => {
const deviceId = route.query.id
const deviceNo = route.query.device_no
if (deviceId) {
fetchDetail(Number(deviceId))
} else if (deviceNo) {
fetchDetail(undefined, String(deviceNo))
} else {
ElMessage.error('缺少设备参数')
handleBack()
}
})
</script>
<style scoped lang="scss">
.device-detail-page {
.device-detail {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
.cards-section {
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
</style>

View File

@@ -801,27 +801,14 @@
remark: ''
})
// 查看设备详情(通过弹窗)
const goToDeviceSearchDetail = async (deviceNo: string) => {
deviceDetailDialogVisible.value = true
deviceDetailLoading.value = true
currentDeviceDetail.value = null
try {
const res = await DeviceService.getDeviceByImei(deviceNo)
if (res.code === 0 && res.data) {
currentDeviceDetail.value = res.data
} else {
ElMessage.error(res.message || '查询失败')
deviceDetailDialogVisible.value = false
// 跳转到设备详情页面
const goToDeviceSearchDetail = (deviceNo: string) => {
router.push({
path: '/asset-management/device-detail',
query: {
device_no: deviceNo
}
} catch (error: any) {
console.error('查询设备详情失败:', error)
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确')
deviceDetailDialogVisible.value = false
} finally {
deviceDetailLoading.value = false
}
})
}
// 查看设备绑定的卡片
@@ -1012,8 +999,11 @@
return h(
'span',
{
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
onClick: () => goToDeviceSearchDetail(row.device_no)
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
goToDeviceSearchDetail(row.device_no)
}
},
row.device_no
)

View File

@@ -0,0 +1,312 @@
<template>
<div class="iot-card-detail-page">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">IoT卡详情</h2>
<div class="header-actions">
<ElButton type="primary" @click="handleRefresh" :loading="loading">
<Icon name="refresh" /> 刷新
</ElButton>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading && !cardDetail" class="loading-container">
<ElIcon class="is-loading" :size="60"><Loading /></ElIcon>
<div class="loading-text">加载中...</div>
</div>
<!-- 详情内容 -->
<DetailPage v-if="cardDetail" :sections="detailSections" :data="cardDetail" />
<!-- 未找到卡片 -->
<div v-if="!cardDetail && !loading" class="empty-container">
<ElEmpty description="未找到该卡片信息" />
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage, ElIcon, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
defineOptions({ name: 'IotCardDetail' })
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const cardDetail = ref<any>(null)
const iccid = ref<string>('')
onMounted(() => {
iccid.value = (route.query.iccid as string) || ''
if (iccid.value) {
loadCardDetail()
} else {
ElMessage.error('缺少ICCID参数')
}
})
// 加载卡片详情
const loadCardDetail = async () => {
loading.value = true
try {
const res = await CardService.getIotCardDetailByIccid(iccid.value)
if (res.code === 0) {
cardDetail.value = res.data
} else {
ElMessage.error(res.msg || '获取卡片详情失败')
}
} catch (error) {
console.error('获取卡片详情失败:', error)
ElMessage.error('获取卡片详情失败')
} finally {
loading.value = false
}
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 刷新
const handleRefresh = () => {
loadCardDetail()
}
// 运营商类型文本
const getCarrierTypeText = (type: string) => {
const typeMap: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
}
return typeMap[type] || type || '--'
}
// 卡业务类型文本
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
return categoryMap[category] || category || '--'
}
// 状态文本
const getStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
return statusMap[status] || '未知'
}
// 状态标签类型
const getStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return typeMap[status] || 'info'
}
// 格式化价格
const formatCardPrice = (price: number) => {
return `¥${((price || 0) / 100).toFixed(2)}`
}
// DetailPage 配置
const detailSections: DetailSection[] = [
{
title: '基本信息',
fields: [
{ label: '卡ID', prop: 'id' },
{
label: 'ICCID',
render: (data) => {
return h(
'span',
{
style: {
padding: '3px 8px',
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
fontSize: '13px',
fontWeight: '500',
color: 'var(--el-text-color-regular)',
background: 'var(--el-fill-color-light)',
border: '1px solid var(--el-border-color-lighter)',
borderRadius: '4px'
}
},
data.iccid
)
}
},
{
label: '卡接入号',
render: (data) => {
return h(
'span',
{
style: {
padding: '3px 8px',
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
fontSize: '13px',
fontWeight: '500',
color: 'var(--el-text-color-regular)',
background: 'var(--el-fill-color-light)',
border: '1px solid var(--el-border-color-lighter)',
borderRadius: '4px'
}
},
data.msisdn || '--'
)
}
},
{ label: '运营商', prop: 'carrier_name', formatter: (value) => value || '--' },
{
label: '运营商类型',
prop: 'carrier_type',
formatter: (value) => getCarrierTypeText(value)
},
{
label: '卡业务类型',
prop: 'card_category',
formatter: (value) => getCardCategoryText(value)
},
{
label: '状态',
render: (data) => {
return h(ElTag, { type: getStatusTagType(data.status) }, () => getStatusText(data.status))
}
},
{ label: '套餐系列', prop: 'series_name', formatter: (value) => value || '--' },
{
label: '激活状态',
render: (data) => {
return h(
ElTag,
{ type: data.activation_status === 1 ? 'success' : 'info' },
() => (data.activation_status === 1 ? '已激活' : '未激活')
)
}
},
{
label: '实名状态',
render: (data) => {
return h(
ElTag,
{ type: data.real_name_status === 1 ? 'success' : 'warning' },
() => (data.real_name_status === 1 ? '已实名' : '未实名')
)
}
},
{
label: '网络状态',
render: (data) => {
return h(
ElTag,
{ type: data.network_status === 1 ? 'success' : 'danger' },
() => (data.network_status === 1 ? '开机' : '停机')
)
}
},
{
label: '累计流量使用',
prop: 'data_usage_mb',
formatter: (value) => `${value} MB`
},
{
label: '一次性佣金',
render: (data) => {
return h(
ElTag,
{ type: data.first_commission_paid ? 'success' : 'info' },
() => (data.first_commission_paid ? '已产生' : '未产生')
)
}
},
{
label: '累计充值',
prop: 'accumulated_recharge',
formatter: (value) => formatCardPrice(value)
},
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '--' },
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
}
]
</script>
<style lang="scss" scoped>
.iot-card-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
flex: 1;
}
.header-actions {
margin-left: auto;
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
.loading-text {
font-size: 14px;
}
}
.empty-container {
padding: 60px 20px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -610,6 +610,7 @@
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import type { PackageSeriesResponse } from '@/types/api'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'StandaloneCardList' })
@@ -909,26 +910,17 @@
}
}
// 打开卡详情弹窗
const goToCardDetail = async (iccid: string) => {
cardDetailDialogVisible.value = true
cardDetailLoading.value = true
currentCardDetail.value = null
try {
const res = await CardService.getIotCardDetailByIccid(iccid)
if (res.code === 0 && res.data) {
currentCardDetail.value = res.data
} else {
ElMessage.error(res.message || '查询失败')
cardDetailDialogVisible.value = false
}
} catch (error: any) {
console.error('查询卡片详情失败:', error)
ElMessage.error(error?.message || '查询失败请检查ICCID是否正确')
cardDetailDialogVisible.value = false
} finally {
cardDetailLoading.value = false
// 跳转到IoT卡详情页面
const goToCardDetail = (iccid: string) => {
if (hasAuth('iot_card:view_detail')) {
router.push({
path: RoutesAlias.StandaloneCardList + '/detail',
query: {
iccid: iccid
}
})
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
@@ -985,8 +977,11 @@
return h(
'span',
{
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
onClick: () => goToCardDetail(row.iccid)
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
goToCardDetail(row.iccid)
}
},
row.iccid
)

View File

@@ -2,58 +2,19 @@
<ArtTableFullScreen>
<div class="task-detail-page" id="table-full-screen">
<ElCard shadow="never" class="art-table-card">
<!-- 返回按钮 -->
<div class="back-button-wrapper">
<ElButton @click="goBack" icon="ArrowLeft">返回</ElButton>
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="goBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">任务详情</h2>
</div>
<!-- 任务基本信息 -->
<ElDescriptions title="任务基本信息" :column="3" border class="task-info">
<ElDescriptionsItem label="任务编号">{{ taskDetail?.task_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="任务类型">
<ElTag :type="taskType === 'device' ? 'warning' : 'primary'" size="small">
{{ taskType === 'device' ? '设备导入' : 'ICCID导入' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ taskDetail?.batch_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商" v-if="taskType === 'card'">
{{ (taskDetail as IotCardImportTaskDetail)?.carrier_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="文件名">{{ taskDetail?.file_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="任务状态">
<ElTag :type="getStatusType(taskDetail?.status)" v-if="taskDetail">
{{ taskDetail.status_text }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ taskDetail?.created_at ? formatDateTime(taskDetail.created_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始处理时间">
{{ taskDetail?.started_at ? formatDateTime(taskDetail.started_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="完成时间">
{{ taskDetail?.completed_at ? formatDateTime(taskDetail.completed_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="错误信息" v-if="taskDetail?.error_message">
<span class="error-message">{{ taskDetail.error_message }}</span>
</ElDescriptionsItem>
</ElDescriptions>
<!-- 统计信息 -->
<ElDescriptions title="统计信息" :column="4" border class="statistics-info">
<ElDescriptionsItem label="总数">
<span class="count-text">{{ taskDetail?.total_count || 0 }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ taskDetail?.success_count || 0 }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ taskDetail?.fail_count || 0 }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="跳过数">
<ElTag type="warning">{{ taskDetail?.skip_count || 0 }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<!-- 使用 DetailPage 组件显示任务信息 -->
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
<!-- 失败记录 -->
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
@@ -86,20 +47,16 @@
</template>
<script setup lang="ts">
import { h, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { CardService, DeviceService } from '@/api/modules'
import {
ElMessage,
ElTag,
ElDescriptions,
ElDescriptionsItem,
ElDivider,
ElTable,
ElTableColumn
} from 'element-plus'
import { ElMessage, ElTag, ElDivider, ElTable, ElTableColumn, ElIcon } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
import type { DeviceImportTaskDetail } from '@/types/api/device'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
defineOptions({ name: 'TaskDetail' })
@@ -130,6 +87,118 @@
}
}
// 详情页配置
const detailSections = computed((): DetailSection[] => {
const sections: DetailSection[] = [
{
title: '任务基本信息',
fields: [
{ label: '任务编号', prop: 'task_no', formatter: (value) => value || '-' },
{
label: '任务类型',
render: () => {
return h(
ElTag,
{ type: taskType.value === 'device' ? 'warning' : 'primary', size: 'small' },
() => (taskType.value === 'device' ? '设备导入' : 'ICCID导入')
)
}
},
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
{ label: '文件名', prop: 'file_name', formatter: (value) => value || '-' },
{
label: '任务状态',
render: (data: TaskDetail) => {
return h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
}
},
...(taskType.value === 'card'
? [
{
label: '运营商',
prop: 'carrier_name',
formatter: (value: any) => value || '-'
}
]
: []),
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '开始处理时间',
prop: 'started_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '完成时间',
prop: 'completed_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
...(taskDetail.value?.error_message
? [
{
label: '错误信息',
prop: 'error_message',
fullWidth: true,
render: (data: TaskDetail) => {
return h(
'span',
{ style: { color: 'var(--el-color-danger)' } },
data.error_message || ''
)
}
}
]
: [])
]
},
{
title: '统计信息',
columns: 2,
fields: [
{
label: '总数',
render: (data: TaskDetail) => {
return h(
'span',
{
style: {
fontSize: '16px',
fontWeight: 'bold',
color: 'var(--el-color-primary)'
}
},
String(data.total_count || 0)
)
}
},
{
label: '成功数',
render: (data: TaskDetail) => {
return h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
}
},
{
label: '失败数',
render: (data: TaskDetail) => {
return h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
}
},
{
label: '跳过数',
render: (data: TaskDetail) => {
return h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
}
}
]
}
]
return sections
})
// 返回列表
const goBack = () => {
router.back()
@@ -181,26 +250,18 @@
<style lang="scss" scoped>
.task-detail-page {
.back-button-wrapper {
margin-bottom: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.task-info {
margin-bottom: 20px;
}
.statistics-info {
margin-bottom: 20px;
}
.count-text {
font-size: 16px;
font-weight: bold;
color: var(--el-color-primary);
}
.error-message {
color: var(--el-color-danger);
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.failure-section,

View File

@@ -0,0 +1,495 @@
<template>
<div class="device-detail-page">
<!-- 页面头部 -->
<ElPageHeader :icon="ArrowLeft" title="返回" @back="handleBack">
<template #content>
<span class="page-title">设备详情</span>
</template>
<template #extra>
<ElButton type="primary" @click="handleRefresh" :loading="loading">
<Icon name="refresh" /> 刷新
</ElButton>
</template>
</ElPageHeader>
<!-- 加载状态 -->
<div v-if="loading && !deviceDetail" class="loading-container">
<ElIcon class="is-loading" :size="60"><Loading /></ElIcon>
<div class="loading-text">加载中...</div>
</div>
<!-- 详情内容 -->
<div v-else-if="deviceDetail" class="detail-content">
<!-- 设备基本信息卡片 -->
<ElCard shadow="never" class="info-card">
<template #header>
<div class="card-header">
<span class="header-title">
<Icon name="mobile" /> 设备基本信息
</span>
</div>
</template>
<ElDescriptions :column="3" border>
<ElDescriptionsItem label="当前卡号">
<span class="code-text">{{ deviceDetail.currentCardNumber }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="当前运营商">
<ElTag :type="getOperatorType(deviceDetail.currentOperator)">
{{ deviceDetail.currentOperator }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="当前IMEI">
<span class="code-text">{{ deviceDetail.currentIMEI }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="设备号码">
<span class="code-text">{{ deviceDetail.deviceNumber }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="设备代理">
{{ deviceDetail.deviceAgent || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="过期时间">
<span :class="{ 'expired-date': isExpired(deviceDetail.expiryTime) }">
{{ deviceDetail.expiryTime }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="getStatusType(deviceDetail.realNameStatus)">
{{ deviceDetail.realNameStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="运营商状态">
<ElTag :type="getStatusType(deviceDetail.operatorStatus)">
{{ deviceDetail.operatorStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="设备状态">
<ElTag :type="getStatusType(deviceDetail.deviceStatus)">
{{ deviceDetail.deviceStatus }}
</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<!-- 流量统计卡片 -->
<ElCard shadow="never" class="info-card">
<template #header>
<div class="card-header">
<span class="header-title">
<Icon name="chart-bar" /> 流量统计
</span>
</div>
</template>
<div class="traffic-stats-grid">
<div class="stat-item total">
<div class="stat-label">总计流量</div>
<div class="stat-value">{{ deviceDetail.totalTraffic }}</div>
</div>
<div class="stat-item used">
<div class="stat-label">已用流量</div>
<div class="stat-value">{{ deviceDetail.usedTraffic }}</div>
<ElProgress
:percentage="getUsagePercentage(deviceDetail.usedTraffic, deviceDetail.totalTraffic)"
:color="
getTrafficColor(
getUsagePercentage(deviceDetail.usedTraffic, deviceDetail.totalTraffic)
)
"
:stroke-width="8"
/>
</div>
<div class="stat-item remaining">
<div class="stat-label">剩余流量</div>
<div class="stat-value">{{ deviceDetail.remainingTraffic }}</div>
</div>
</div>
</ElCard>
<!-- 双卡信息卡片 -->
<ElCard shadow="never" class="info-card">
<template #header>
<div class="card-header">
<span class="header-title">
<Icon name="credit-card" /> 双卡信息
</span>
</div>
</template>
<div class="dual-cards-container">
<!-- 卡1信息 -->
<div class="card-column">
<div class="card-title">卡1信息</div>
<ElDescriptions :column="1" border>
<ElDescriptionsItem label="ICCID">
<span class="code-text">{{ deviceDetail.card1.iccid }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="接入号">
<span class="code-text">{{ deviceDetail.card1.accessNumber }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="虚拟号">
<span class="code-text">{{ deviceDetail.card1.virtualNumber }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="IMEI">
<span class="code-text">{{ deviceDetail.card1.imei }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="代理商">
{{ deviceDetail.card1.agent || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="getStatusType(deviceDetail.card1.realNameStatus)">
{{ deviceDetail.card1.realNameStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">
<ElTag :type="getOperatorType(deviceDetail.card1.operator)">
{{ deviceDetail.card1.operator }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="网卡状态">
<ElTag :type="getStatusType(deviceDetail.card1.cardStatus)">
{{ deviceDetail.card1.cardStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="过期时间">
{{ deviceDetail.card1.expiryTime }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">
{{ deviceDetail.card1.packageSeries || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="总流量">
{{ deviceDetail.card1.totalTraffic }}
</ElDescriptionsItem>
<ElDescriptionsItem label="已使用流量">
<span class="traffic-info used">{{ deviceDetail.card1.usedTraffic }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="剩余流量">
<span class="traffic-info remaining">{{ deviceDetail.card1.remainingTraffic }}</span>
</ElDescriptionsItem>
</ElDescriptions>
</div>
<!-- 卡2信息 -->
<div class="card-column">
<div class="card-title">卡2信息</div>
<ElDescriptions :column="1" border>
<ElDescriptionsItem label="ICCID">
<span class="code-text">{{ deviceDetail.card2.iccid }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="接入号">
<span class="code-text">{{ deviceDetail.card2.accessNumber }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="虚拟号">
<span class="code-text">{{ deviceDetail.card2.virtualNumber || '--' }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="IMEI">
<span class="code-text">{{ deviceDetail.card2.imei }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="代理商">
{{ deviceDetail.card2.agent || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="getStatusType(deviceDetail.card2.realNameStatus)">
{{ deviceDetail.card2.realNameStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">
<ElTag :type="getOperatorType(deviceDetail.card2.operator)">
{{ deviceDetail.card2.operator }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="网卡状态">
<ElTag :type="getStatusType(deviceDetail.card2.cardStatus)">
{{ deviceDetail.card2.cardStatus }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="过期时间">
{{ deviceDetail.card2.expiryTime || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">
{{ deviceDetail.card2.packageSeries || '--' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="总流量">
{{ deviceDetail.card2.totalTraffic }}
</ElDescriptionsItem>
<ElDescriptionsItem label="已使用流量">
<span class="traffic-info used">{{ deviceDetail.card2.usedTraffic }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="剩余流量">
<span class="traffic-info remaining">{{ deviceDetail.card2.remainingTraffic }}</span>
</ElDescriptionsItem>
</ElDescriptions>
</div>
</div>
</ElCard>
</div>
<!-- 未找到设备 -->
<ElCard v-else shadow="never" class="empty-card">
<ElEmpty description="未找到该设备信息" />
</ElCard>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
import { DeviceService } from '@/api/modules'
import { ElMessage, ElIcon } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
defineOptions({ name: 'DeviceDetail' })
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const deviceDetail = ref<any>(null)
const iccid = ref<string>('')
onMounted(() => {
iccid.value = (route.query.iccid as string) || ''
if (iccid.value) {
loadDeviceDetail()
} else {
ElMessage.error('缺少ICCID参数')
}
})
// 加载设备详情
const loadDeviceDetail = async () => {
loading.value = true
try {
const res = await DeviceService.getDeviceByIccid(iccid.value)
if (res.code === 0) {
deviceDetail.value = res.data
} else {
ElMessage.error(res.msg || '获取设备详情失败')
}
} catch (error) {
console.error('获取设备详情失败:', error)
ElMessage.error('获取设备详情失败')
} finally {
loading.value = false
}
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 刷新
const handleRefresh = () => {
loadDeviceDetail()
}
// 获取运营商标签类型
const getOperatorType = (operator: string) => {
switch (operator) {
case '中国移动':
case 'gs联动1':
return 'success'
case '中国联通':
return 'primary'
case '中国电信':
case 'gs电信':
return 'warning'
default:
return 'info'
}
}
// 获取状态标签类型
const getStatusType = (status: string) => {
switch (status) {
case '正常':
case '已实名':
case '在线':
return 'success'
case '停机':
case '离线':
return 'danger'
case '未实名':
return 'warning'
case '接口异常':
return 'info'
default:
return 'info'
}
}
// 判断是否过期
const isExpired = (expiryTime: string) => {
return new Date(expiryTime) < new Date()
}
// 计算流量使用百分比
const getUsagePercentage = (used: string, total: string) => {
const usedGB = parseFloat(used.replace('GB', ''))
const totalGB = parseFloat(total.replace('GB', ''))
return Math.round((usedGB / totalGB) * 100)
}
// 获取流量进度条颜色
const getTrafficColor = (percentage: number) => {
if (percentage < 50) return 'var(--el-color-success)'
if (percentage < 80) return 'var(--el-color-warning)'
return 'var(--el-color-danger)'
}
</script>
<style lang="scss" scoped>
.device-detail-page {
padding: 20px;
.page-title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
.loading-text {
margin-top: 16px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.detail-content {
margin-top: 20px;
.info-card {
margin-bottom: 20px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-title {
display: flex;
gap: 8px;
align-items: center;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.code-text {
padding: 3px 8px;
font-family: 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace;
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.traffic-info {
font-weight: 600;
&.used {
color: var(--el-color-warning);
}
&.remaining {
color: var(--el-color-success);
}
}
}
}
.traffic-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
.stat-item {
padding: 20px;
text-align: center;
background: var(--el-fill-color-extra-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
.stat-label {
margin-bottom: 10px;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.stat-value {
margin-bottom: 12px;
font-size: 18px;
font-weight: 600;
}
&.total .stat-value {
color: var(--el-color-primary);
}
&.used .stat-value {
color: var(--el-color-warning);
}
&.remaining .stat-value {
color: var(--el-color-success);
}
}
}
.dual-cards-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
.card-column {
.card-title {
display: inline-block;
padding-bottom: 10px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
border-bottom: 2px solid var(--el-color-primary);
}
}
}
.empty-card {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.expired-date {
font-weight: 600;
color: var(--el-color-danger);
}
}
:deep(.el-page-header) {
margin-bottom: 20px;
}
:deep(.el-descriptions__label) {
font-weight: 500;
}
// 响应式设计
@media (width <= 1200px) {
.dual-cards-container {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -62,7 +62,7 @@
</div>
</div>
<div class="info-item">
<div class="info-label">设备号码</div>
<div class="info-label">设备号码:</div>
<div class="info-value">
<span class="code-text">{{ deviceInfo.deviceNumber }}</span>
</div>
@@ -153,7 +153,9 @@
<div class="card-info-list">
<div class="card-info-item">
<span class="label">ICCID</span>
<span class="value code-text">{{ deviceInfo.card1.iccid }}</span>
<span class="value code-text clickable" @click.stop="goToDetail(deviceInfo.card1.iccid)">
{{ deviceInfo.card1.iccid }}
</span>
</div>
<div class="card-info-item">
<span class="label">接入号</span>
@@ -265,7 +267,9 @@
<div class="card-info-list">
<div class="card-info-item">
<span class="label">ICCID</span>
<span class="value code-text">{{ deviceInfo.card2.iccid }}</span>
<span class="value code-text clickable" @click.stop="goToDetail(deviceInfo.card2.iccid)">
{{ deviceInfo.card2.iccid }}
</span>
</div>
<div class="card-info-item">
<span class="label">接入号</span>
@@ -755,9 +759,11 @@
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
defineOptions({ name: 'DeviceManagement' })
const router = useRouter()
const iccidSearch = ref('')
const loading = ref(false)
const searched = ref(false)
@@ -1102,6 +1108,16 @@
ElMessage.success(`对卡${cardNumber}执行${operationMap[operation]}操作`)
}
// 跳转到设备详情页
const goToDetail = (iccid: string) => {
router.push({
path: '/asset-management/device-detail',
query: {
iccid: iccid
}
})
}
// 订单历史数据
const orderHistory = reactive([
{
@@ -1346,6 +1362,16 @@
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
&.clickable {
color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline;
&:hover {
opacity: 0.8;
}
}
}
.expired-date {