This commit is contained in:
@@ -28,15 +28,21 @@
|
||||
: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"
|
||||
@@ -57,16 +63,28 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
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>>()
|
||||
@@ -374,4 +392,8 @@
|
||||
.asset-allocation-records-page {
|
||||
// Allocation records page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,15 +28,21 @@
|
||||
: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"
|
||||
@@ -82,9 +88,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
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'
|
||||
import type {
|
||||
AuthorizationItem,
|
||||
@@ -98,6 +106,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const remarkDialogVisible = ref(false)
|
||||
const remarkLoading = ref(false)
|
||||
@@ -442,4 +460,8 @@
|
||||
.authorization-records-page {
|
||||
// Authorization records page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
<template>
|
||||
<div class="device-detail">
|
||||
<ElCard shadow="never">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<ElButton @click="handleBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 绑定卡对话框 -->
|
||||
<ElDialog v-model="bindDialogVisible" title="绑定卡到设备" width="500px">
|
||||
<ElForm ref="bindFormRef" :model="bindForm" :rules="bindRules" label-width="100px">
|
||||
<ElFormItem label="选择卡" prop="iot_card_id">
|
||||
<ElSelect
|
||||
v-model="bindForm.iot_card_id"
|
||||
placeholder="请搜索并选择卡"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchCards"
|
||||
:loading="searchCardsLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="card in availableCards"
|
||||
:key="card.id"
|
||||
:label="`${card.iccid} - ${card.carrier_name}`"
|
||||
:value="card.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ card.iccid }}</span>
|
||||
<ElTag size="small" type="info">{{ card.carrier_name }}</ElTag>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="插槽位置" prop="slot_position">
|
||||
<ElSelect
|
||||
v-model="bindForm.slot_position"
|
||||
placeholder="请选择插槽位置"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="slot in availableSlots"
|
||||
:key="slot"
|
||||
:label="`插槽 ${slot}`"
|
||||
:value="slot"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="bindDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirmBind" :loading="bindLoading">
|
||||
确认绑定
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</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 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 detailData = ref<Device | null>(null)
|
||||
const cardList = ref<DeviceCardBinding[]>([])
|
||||
const availableCards = ref<any[]>([])
|
||||
|
||||
// 状态类型映射
|
||||
const statusTypeMap: Record<number, any> = {
|
||||
1: 'info',
|
||||
2: 'primary',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
|
||||
// 卡状态类型映射
|
||||
const cardStatusTypeMap: Record<number, any> = {
|
||||
1: 'info',
|
||||
2: 'primary',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
|
||||
// 卡状态文本映射
|
||||
const cardStatusTextMap: Record<number, string> = {
|
||||
1: '在库',
|
||||
2: '已分销',
|
||||
3: '已激活',
|
||||
4: '已停用'
|
||||
}
|
||||
|
||||
// 绑定表单
|
||||
const bindForm = reactive({
|
||||
iot_card_id: undefined as number | undefined,
|
||||
slot_position: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 绑定表单验证规则
|
||||
const bindRules = reactive<FormRules>({
|
||||
iot_card_id: [{ required: true, message: '请选择卡', trigger: 'change' }],
|
||||
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 可用插槽
|
||||
const availableSlots = computed(() => {
|
||||
if (!detailData.value) return []
|
||||
const occupiedSlots = cardList.value.map((card) => card.slot_position)
|
||||
const allSlots = Array.from({ length: detailData.value.max_sim_slots }, (_, i) => i + 1)
|
||||
return allSlots.filter((slot) => !occupiedSlots.includes(slot))
|
||||
})
|
||||
|
||||
// 详情页配置
|
||||
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 handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 获取详情数据
|
||||
const fetchDetail = async (id?: number, deviceNo?: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
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('获取设备详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设备绑定的卡列表
|
||||
const loadDeviceCards = async (id: number) => {
|
||||
cardsLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDeviceCards(id)
|
||||
if (res.code === 0) {
|
||||
cardList.value = res.data.bindings || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取绑定卡列表失败')
|
||||
} finally {
|
||||
cardsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索可用的卡
|
||||
const searchCards = async (query: string) => {
|
||||
if (!query) {
|
||||
availableCards.value = []
|
||||
return
|
||||
}
|
||||
searchCardsLoading.value = true
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
iccid: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
if (res.code === 0) {
|
||||
availableCards.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
searchCardsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示绑定对话框
|
||||
const showBindDialog = () => {
|
||||
bindForm.iot_card_id = undefined
|
||||
bindForm.slot_position = undefined
|
||||
availableCards.value = []
|
||||
bindDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 确认绑定
|
||||
const handleConfirmBind = async () => {
|
||||
if (!bindFormRef.value || !detailData.value) return
|
||||
|
||||
await bindFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
bindLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
iot_card_id: bindForm.iot_card_id!,
|
||||
slot_position: bindForm.slot_position!
|
||||
}
|
||||
const res = await DeviceService.bindCard(detailData.value!.id, data)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('绑定成功')
|
||||
bindDialogVisible.value = false
|
||||
await fetchDetail(detailData.value!.id)
|
||||
if (bindFormRef.value) {
|
||||
bindFormRef.value.resetFields()
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '绑定失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('绑定失败')
|
||||
} finally {
|
||||
bindLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑卡
|
||||
const handleUnbindCard = (card: DeviceCardBinding) => {
|
||||
ElMessageBox.confirm(`确定解绑卡 ${card.iccid} 吗?`, '解绑确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await DeviceService.unbindCard(detailData.value!.id, card.iot_card_id)
|
||||
ElMessage.success('解绑成功')
|
||||
await fetchDetail(detailData.value!.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('解绑失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
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>
|
||||
@@ -53,10 +53,13 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -64,6 +67,9 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
||||
<ElForm
|
||||
@@ -570,8 +576,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
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'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
@@ -581,6 +589,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
const recallLoading = ref(false)
|
||||
@@ -801,11 +819,11 @@
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 跳转到设备详情页面
|
||||
// 跳转到资产信息页面(原设备详情)
|
||||
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||||
if (hasAuth('device:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/device-detail',
|
||||
path: '/asset-management/single-card',
|
||||
query: {
|
||||
device_no: deviceNo
|
||||
}
|
||||
@@ -1697,4 +1715,8 @@
|
||||
.device-list-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,15 +39,21 @@
|
||||
: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"
|
||||
@@ -125,9 +131,11 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
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'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
||||
@@ -137,6 +145,16 @@
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
@@ -679,4 +697,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
<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>
|
||||
@@ -51,7 +51,7 @@
|
||||
hasAuth('iot_card:batch_download')
|
||||
"
|
||||
type="info"
|
||||
@contextmenu.prevent="showMoreMenu"
|
||||
@click="showMoreMenuOnClick"
|
||||
>
|
||||
更多操作
|
||||
</ElButton>
|
||||
@@ -68,10 +68,13 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
@@ -79,6 +82,9 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 批量分配对话框 -->
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
@@ -597,8 +603,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
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'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import type {
|
||||
@@ -616,6 +624,16 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
@@ -910,11 +928,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到IoT卡详情页面
|
||||
// 跳转到资产信息页面(原IoT卡详情)
|
||||
const goToCardDetail = (iccid: string) => {
|
||||
if (hasAuth('iot_card:view_detail')) {
|
||||
router.push({
|
||||
path: RoutesAlias.StandaloneCardList + '/detail',
|
||||
path: '/asset-management/single-card',
|
||||
query: {
|
||||
iccid: iccid
|
||||
}
|
||||
@@ -1575,13 +1593,19 @@
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示更多操作菜单
|
||||
// 显示更多操作菜单 (右键)
|
||||
const showMoreMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
moreMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 显示更多操作菜单 (左键点击)
|
||||
const showMoreMenuOnClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
moreMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理更多操作菜单选择
|
||||
const handleMoreMenuSelect = (item: MenuItemType) => {
|
||||
switch (item.key) {
|
||||
@@ -1829,4 +1853,8 @@
|
||||
.standalone-card-list-page {
|
||||
// Card list page styles
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,15 +39,21 @@
|
||||
: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"
|
||||
@@ -139,8 +145,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
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'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -152,6 +160,16 @@
|
||||
const router = useRouter()
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
@@ -733,4 +751,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user