fetch(add): 分配记录,批量分配/回收, 单卡列表, 任务列表, 导入ICCID
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m21s

This commit is contained in:
sexygoat
2026-01-24 16:18:30 +08:00
parent c69124a819
commit 0eed8244e5
13 changed files with 2375 additions and 334 deletions

View File

@@ -269,4 +269,86 @@ export class CardService extends BaseService {
static getCardChangeNotices(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/card-change-notices', params)
}
// ========== ICCID批量导入相关 ==========
/**
* 批量导入ICCID
* @param file Excel文件
* @param carrier_id 运营商ID
* @param batch_no 批次号(可选)
*/
static importIotCards(
file: File,
carrier_id: number,
batch_no?: string
): Promise<BaseResponse> {
const formData = new FormData()
formData.append('file', file)
formData.append('carrier_id', carrier_id.toString())
if (batch_no) {
formData.append('batch_no', batch_no)
}
return this.upload('/api/admin/iot-cards/import', file, { carrier_id, batch_no })
}
/**
* 获取导入任务列表
* @param params 查询参数
*/
static getIotCardImportTasks(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/admin/iot-cards/import-tasks', params)
}
/**
* 获取导入任务详情
* @param id 任务ID
*/
static getIotCardImportTaskDetail(id: number): Promise<BaseResponse<any>> {
return this.getOne(`/api/admin/iot-cards/import-tasks/${id}`)
}
// ========== 单卡列表(未绑定设备)相关 ==========
/**
* 获取单卡列表(未绑定设备)
* @param params 查询参数
*/
static getStandaloneIotCards(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/admin/iot-cards/standalone', params)
}
/**
* 批量分配单卡
* @param data 分配参数
*/
static allocateStandaloneCards(data: any): Promise<BaseResponse<any>> {
return this.post('/api/admin/iot-cards/standalone/allocate', data)
}
/**
* 批量回收单卡
* @param data 回收参数
*/
static recallStandaloneCards(data: any): Promise<BaseResponse<any>> {
return this.post('/api/admin/iot-cards/standalone/recall', data)
}
// ========== 资产分配记录相关 ==========
/**
* 获取资产分配记录列表
* @param params 查询参数
*/
static getAssetAllocationRecords(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/admin/asset-allocation-records', params)
}
/**
* 获取资产分配记录详情
* @param id 记录ID
*/
static getAssetAllocationRecordDetail(id: number): Promise<BaseResponse<any>> {
return this.getOne(`/api/admin/asset-allocation-records/${id}`)
}
}

View File

@@ -16,3 +16,6 @@ export * from './commission'
// 通用状态相关
export * from './status'
// IoT卡相关
export * from './iotCard'

View File

@@ -0,0 +1,141 @@
/**
* IoT卡相关常量配置
*/
// IoT卡导入任务状态枚举
export enum IotCardImportTaskStatus {
PENDING = 1, // 待处理
PROCESSING = 2, // 处理中
COMPLETED = 3, // 已完成
FAILED = 4 // 失败
}
// IoT卡导入任务状态选项
export const IOT_CARD_IMPORT_TASK_STATUS_OPTIONS = [
{
label: '待处理',
value: IotCardImportTaskStatus.PENDING,
type: 'info' as const,
color: '#909399'
},
{
label: '处理中',
value: IotCardImportTaskStatus.PROCESSING,
type: 'warning' as const,
color: '#E6A23C'
},
{
label: '已完成',
value: IotCardImportTaskStatus.COMPLETED,
type: 'success' as const,
color: '#67C23A'
},
{
label: '失败',
value: IotCardImportTaskStatus.FAILED,
type: 'danger' as const,
color: '#F56C6C'
}
]
// IoT卡导入任务状态映射
export const IOT_CARD_IMPORT_TASK_STATUS_MAP = IOT_CARD_IMPORT_TASK_STATUS_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<
IotCardImportTaskStatus,
{
label: string
value: IotCardImportTaskStatus
type: 'info' | 'warning' | 'success' | 'danger'
color: string
}
>
)
// 获取IoT卡导入任务状态标签
export function getIotCardImportTaskStatusLabel(status: number): string {
return IOT_CARD_IMPORT_TASK_STATUS_MAP[status as IotCardImportTaskStatus]?.label || '未知'
}
// 获取IoT卡导入任务状态类型用于 ElTag
export function getIotCardImportTaskStatusType(status: number) {
return IOT_CARD_IMPORT_TASK_STATUS_MAP[status as IotCardImportTaskStatus]?.type || 'info'
}
// 获取IoT卡导入任务状态颜色
export function getIotCardImportTaskStatusColor(status: number): string {
return IOT_CARD_IMPORT_TASK_STATUS_MAP[status as IotCardImportTaskStatus]?.color || '#909399'
}
// ========== 单卡状态相关 ==========
// 单卡状态枚举
export enum StandaloneCardStatus {
IN_STOCK = 1, // 在库
DISTRIBUTED = 2, // 已分销
ACTIVATED = 3, // 已激活
DEACTIVATED = 4 // 已停用
}
// 单卡状态选项
export const STANDALONE_CARD_STATUS_OPTIONS = [
{
label: '在库',
value: StandaloneCardStatus.IN_STOCK,
type: 'info' as const,
color: '#909399'
},
{
label: '已分销',
value: StandaloneCardStatus.DISTRIBUTED,
type: 'warning' as const,
color: '#E6A23C'
},
{
label: '已激活',
value: StandaloneCardStatus.ACTIVATED,
type: 'success' as const,
color: '#67C23A'
},
{
label: '已停用',
value: StandaloneCardStatus.DEACTIVATED,
type: 'danger' as const,
color: '#F56C6C'
}
]
// 单卡状态映射
export const STANDALONE_CARD_STATUS_MAP = STANDALONE_CARD_STATUS_OPTIONS.reduce(
(map, item) => {
map[item.value] = item
return map
},
{} as Record<
StandaloneCardStatus,
{
label: string
value: StandaloneCardStatus
type: 'info' | 'warning' | 'success' | 'danger'
color: string
}
>
)
// 获取单卡状态标签
export function getStandaloneCardStatusLabel(status: number): string {
return STANDALONE_CARD_STATUS_MAP[status as StandaloneCardStatus]?.label || '未知'
}
// 获取单卡状态类型(用于 ElTag
export function getStandaloneCardStatusType(status: number) {
return STANDALONE_CARD_STATUS_MAP[status as StandaloneCardStatus]?.type || 'info'
}
// 获取单卡状态颜色
export function getStandaloneCardStatusColor(status: number): string {
return STANDALONE_CARD_STATUS_MAP[status as StandaloneCardStatus]?.color || '#909399'
}

View File

@@ -435,9 +435,11 @@
"assetManagement": {
"title": "资产管理",
"singleCard": "单卡信息",
"cardList": "网卡管理",
"standaloneCardList": "单卡列表",
"taskManagement": "任务管理",
"taskDetail": "任务详情",
"devices": "设备管理",
"assetAssign": "资产分配",
"assetAssign": "分配记录",
"cardReplacementRequest": "换卡申请"
},
"account": {

View File

@@ -844,13 +844,32 @@ export const asyncRoutes: AppRouteRecord[] = [
},
{
path: 'card-list',
name: 'CardList',
component: RoutesAlias.CardList,
name: 'StandaloneCardList',
component: RoutesAlias.StandaloneCardList,
meta: {
title: 'menus.assetManagement.cardList',
title: 'menus.assetManagement.standaloneCardList',
keepAlive: true
}
},
{
path: 'task-management',
name: 'TaskManagement',
component: RoutesAlias.TaskManagement,
meta: {
title: 'menus.assetManagement.taskManagement',
keepAlive: true
}
},
{
path: 'task-detail',
name: 'TaskDetail',
component: RoutesAlias.TaskDetail,
meta: {
title: 'menus.assetManagement.taskDetail',
isHide: true,
keepAlive: false
}
},
{
path: 'devices',
name: 'DeviceList',
@@ -869,6 +888,16 @@ export const asyncRoutes: AppRouteRecord[] = [
keepAlive: true
}
},
{
path: 'allocation-record-detail',
name: 'AllocationRecordDetail',
component: RoutesAlias.AllocationRecordDetail,
meta: {
title: '分配记录详情',
isHide: true,
keepAlive: false
}
},
{
path: 'card-replacement-request',
name: 'CardReplacementRequest',

View File

@@ -92,7 +92,11 @@ export enum RoutesAlias {
SimCardAssign = '/product/sim-card-assign', // 号卡分配
// 资产管理
AssetAssign = '/asset-management/asset-assign', // 资产分配
StandaloneCardList = '/asset-management/card-list', // 单卡列表(未绑定设备)
TaskManagement = '/asset-management/task-management', // 任务管理
TaskDetail = '/asset-management/task-detail', // 任务详情
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
AllocationRecordDetail = '/asset-management/allocation-record-detail', // 分配记录详情
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
// 账户管理

View File

@@ -227,3 +227,225 @@ export interface CardOrder {
createTime: string
payTime?: string
}
// ========== ICCID批量导入相关 ==========
// ICCID导入任务状态枚举
export enum IotCardImportTaskStatus {
PENDING = 1, // 待处理
PROCESSING = 2, // 处理中
COMPLETED = 3, // 已完成
FAILED = 4 // 失败
}
// ICCID导入请求参数
export interface ImportIotCardParams {
carrier_id: number // 运营商ID
batch_no?: string // 批次号
file: File // 文件
}
// ICCID导入响应
export interface ImportIotCardResponse {
message: string
task_id: number
task_no: string
}
// 导入任务记录
export interface IotCardImportTask {
id: number // 任务ID
task_no: string // 任务编号
batch_no: string // 批次号
carrier_id: number // 运营商ID
carrier_name: string // 运营商名称
file_name: string // 文件名
status: IotCardImportTaskStatus // 任务状态
status_text: string // 任务状态文本
total_count: number // 总数
success_count: number // 成功数
fail_count: number // 失败数
skip_count: number // 跳过数
error_message: string // 错误信息
created_at: string // 创建时间
started_at: string | null // 开始处理时间
completed_at: string | null // 完成时间
}
// 导入任务查询参数
export interface IotCardImportTaskQueryParams extends PaginationParams {
status?: IotCardImportTaskStatus // 任务状态
carrier_id?: number // 运营商ID
batch_no?: string // 批次号(模糊查询)
start_time?: string // 创建时间起始
end_time?: string // 创建时间结束
}
// 导入结果详细项
export interface ImportResultItem {
line: number // 行号
iccid: string // ICCID
reason: string // 原因
}
// 导入任务详情
export interface IotCardImportTaskDetail extends IotCardImportTask {
failed_items: ImportResultItem[] | null // 失败记录详情
skipped_items: ImportResultItem[] | null // 跳过记录详情
}
// ========== 单卡列表(未绑定设备)相关 ==========
// 单卡状态枚举
export enum StandaloneCardStatus {
IN_STOCK = 1, // 在库
DISTRIBUTED = 2, // 已分销
ACTIVATED = 3, // 已激活
DEACTIVATED = 4 // 已停用
}
// 单卡查询参数
export interface StandaloneCardQueryParams extends PaginationParams {
status?: StandaloneCardStatus // 状态
carrier_id?: number // 运营商ID
shop_id?: number // 分销商ID
iccid?: string // ICCID(模糊查询)
msisdn?: string // 卡接入号(模糊查询)
batch_no?: string // 批次号
package_id?: number // 套餐ID
is_distributed?: boolean // 是否已分销
is_replaced?: boolean // 是否有换卡记录
iccid_start?: string // ICCID起始号
iccid_end?: string // ICCID结束号
}
// 单卡信息
export interface StandaloneIotCard {
id: number // 卡ID
iccid: string // ICCID
imsi: string // IMSI
msisdn: string // 卡接入号
carrier_id: number // 运营商ID
carrier_name: string // 运营商名称
card_type: string // 卡类型
card_category: string // 卡业务类型 (normal:普通卡, industry:行业卡)
status: StandaloneCardStatus // 状态
activation_status: number // 激活状态 (0:未激活, 1:已激活)
network_status: number // 网络状态 (0:停机, 1:开机)
real_name_status: number // 实名状态 (0:未实名, 1:已实名)
batch_no: string // 批次号
supplier: string // 供应商
shop_id: number | null // 店铺ID
shop_name: string // 店铺名称
cost_price: number // 成本价(分)
distribute_price: number // 分销价(分)
data_usage_mb: number // 累计流量使用(MB)
activated_at: string | null // 激活时间
created_at: string // 创建时间
updated_at: string // 更新时间
}
// ========== 单卡批量分配和回收相关 ==========
// 选卡方式枚举
export enum CardSelectionType {
LIST = 'list', // ICCID列表
RANGE = 'range', // 号段范围
FILTER = 'filter' // 筛选条件
}
// 批量分配单卡请求参数
export interface AllocateStandaloneCardsRequest {
selection_type: CardSelectionType // 选卡方式
to_shop_id: number // 目标店铺ID
iccids?: string[] // ICCID列表selection_type=list时必填
iccid_start?: string // 起始ICCIDselection_type=range时必填
iccid_end?: string // 结束ICCIDselection_type=range时必填
carrier_id?: number // 运营商IDselection_type=filter时可选
status?: StandaloneCardStatus // 卡状态selection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
remark?: string // 备注
}
// 批量回收单卡请求参数
export interface RecallStandaloneCardsRequest {
selection_type: CardSelectionType // 选卡方式
from_shop_id: number // 来源店铺ID被回收方
iccids?: string[] // ICCID列表selection_type=list时必填
iccid_start?: string // 起始ICCIDselection_type=range时必填
iccid_end?: string // 结束ICCIDselection_type=range时必填
carrier_id?: number // 运营商IDselection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
remark?: string // 备注
}
// 分配失败项
export interface AllocationFailedItem {
iccid: string // ICCID
reason: string // 失败原因
}
// 批量分配/回收响应
export interface AllocateStandaloneCardsResponse {
allocation_no: string // 分配/回收单号
total_count: number // 待分配/回收总数
success_count: number // 成功数
fail_count: number // 失败数
failed_items: AllocationFailedItem[] | null // 失败项列表
}
// ========== 资产分配记录相关 ==========
// 分配类型枚举
export enum AllocationTypeEnum {
ALLOCATE = 'allocate', // 分配
RECALL = 'recall' // 回收
}
// 资产类型枚举
export enum AssetTypeEnum {
IOT_CARD = 'iot_card', // 物联网卡
DEVICE = 'device' // 设备
}
// 资产分配记录查询参数
export interface AssetAllocationRecordQueryParams extends PaginationParams {
allocation_type?: AllocationTypeEnum // 分配类型
asset_type?: AssetTypeEnum // 资产类型
asset_identifier?: string // 资产标识符ICCID或设备号
allocation_no?: string // 分配单号
from_shop_id?: number // 来源店铺ID
to_shop_id?: number // 目标店铺ID
operator_id?: number // 操作人ID
created_at_start?: string // 创建时间起始
created_at_end?: string // 创建时间结束
}
// 资产分配记录
export interface AssetAllocationRecord {
id: number // 记录ID
allocation_no: string // 分配单号
allocation_type: AllocationTypeEnum // 分配类型
allocation_name: string // 分配类型名称
asset_type: AssetTypeEnum // 资产类型
asset_type_name: string // 资产类型名称
asset_id: number // 资产ID
asset_identifier: string // 资产标识符ICCID或设备号
from_owner_id: number | null // 来源所有者ID
from_owner_name: string // 来源所有者名称
from_owner_type: string // 来源所有者类型
to_owner_id: number // 目标所有者ID
to_owner_name: string // 目标所有者名称
to_owner_type: string // 目标所有者类型
operator_id: number // 操作人ID
operator_name: string // 操作人名称
related_card_count: number // 关联卡数量
related_device_id: number | null // 关联设备ID
remark: string // 备注
created_at: string // 创建时间
}
// 资产分配记录详情
export interface AssetAllocationRecordDetail extends AssetAllocationRecord {
related_card_ids: number[] // 关联卡ID列表
}

View File

@@ -0,0 +1,162 @@
<template>
<ArtTableFullScreen>
<div class="allocation-record-detail-page" id="table-full-screen">
<ElCard shadow="never" style="margin-bottom: 20px">
<template #header>
<div class="card-header">
<span>分配记录详情</span>
<ElButton @click="goBack">返回</ElButton>
</div>
</template>
<ElSkeleton :loading="loading" :rows="10" animated>
<template #default>
<ElDescriptions v-if="recordDetail" title="基本信息" :column="3" border>
<ElDescriptionsItem label="分配单号">{{ recordDetail.allocation_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="分配类型">
<ElTag :type="getAllocationTypeType(recordDetail.allocation_type)">
{{ recordDetail.allocation_name }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="资产类型">
<ElTag :type="getAssetTypeType(recordDetail.asset_type)">
{{ recordDetail.asset_type_name }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="资产标识符">{{ recordDetail.asset_identifier }}</ElDescriptionsItem>
<ElDescriptionsItem label="关联卡数量">{{ recordDetail.related_card_count }}</ElDescriptionsItem>
<ElDescriptionsItem label="关联设备ID">
{{ recordDetail.related_device_id || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
<ElDescriptions v-if="recordDetail" title="所有者信息" :column="2" border style="margin-top: 20px">
<ElDescriptionsItem label="来源所有者">
{{ recordDetail.from_owner_name }} ({{ recordDetail.from_owner_type }})
</ElDescriptionsItem>
<ElDescriptionsItem label="目标所有者">
{{ recordDetail.to_owner_name }} ({{ recordDetail.to_owner_type }})
</ElDescriptionsItem>
</ElDescriptions>
<ElDescriptions v-if="recordDetail" title="操作信息" :column="2" border style="margin-top: 20px">
<ElDescriptionsItem label="操作人">{{ recordDetail.operator_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(recordDetail.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="备注" :span="2">
{{ recordDetail.remark || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- 关联卡列表 -->
<div v-if="recordDetail && recordDetail.related_card_ids && recordDetail.related_card_ids.length > 0" style="margin-top: 20px">
<ElDivider content-position="left">关联卡列表</ElDivider>
<ElTable :data="relatedCardsList" border>
<ElTableColumn type="index" label="序号" width="60" />
<ElTableColumn prop="card_id" label="卡ID" width="80" />
<ElTableColumn label="ICCID" width="180">
<template #default="scope">
{{ getCardInfo(scope.row.card_id, 'iccid') }}
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="100">
<template #default="scope">
{{ getCardInfo(scope.row.card_id, 'status') }}
</template>
</ElTableColumn>
</ElTable>
</div>
</template>
</ElSkeleton>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage } from 'element-plus'
import { formatDateTime } from '@/utils/business/format'
import type {
AssetAllocationRecordDetail,
AllocationTypeEnum,
AssetTypeEnum
} from '@/types/api/card'
defineOptions({ name: 'AllocationRecordDetail' })
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const recordDetail = ref<AssetAllocationRecordDetail | null>(null)
const relatedCardsList = ref<{ card_id: number }[]>([])
// 获取分配类型标签类型
const getAllocationTypeType = (type: AllocationTypeEnum) => {
return type === 'allocate' ? 'success' : 'warning'
}
// 获取资产类型标签类型
const getAssetTypeType = (type: AssetTypeEnum) => {
return type === 'iot_card' ? 'primary' : 'info'
}
// 模拟获取卡信息的方法(实际应该调用API获取)
const getCardInfo = (cardId: number, field: 'iccid' | 'status') => {
if (field === 'iccid') {
return `ICCID-${cardId}`
} else {
return '在库'
}
}
// 获取详情数据
const getDetailData = async () => {
const id = route.query.id as string
if (!id) {
ElMessage.error('缺少记录ID参数')
goBack()
return
}
loading.value = true
try {
const res = await CardService.getAssetAllocationRecordDetail(Number(id))
if (res.code === 0) {
recordDetail.value = res.data
// 构建关联卡列表
if (recordDetail.value.related_card_ids && recordDetail.value.related_card_ids.length > 0) {
relatedCardsList.value = recordDetail.value.related_card_ids.map((cardId) => ({
card_id: cardId
}))
}
}
} catch (error) {
console.error(error)
ElMessage.error('获取分配记录详情失败')
} finally {
loading.value = false
}
}
// 返回列表
const goBack = () => {
router.back()
}
onMounted(() => {
getDetailData()
})
</script>
<style lang="scss" scoped>
.allocation-record-detail-page {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>

View File

@@ -1,352 +1,324 @@
<template>
<div class="page-content">
<!-- 分配模式选择 -->
<ElCard shadow="never" style="margin-bottom: 20px">
<ElRadioGroup v-model="assignMode" size="large">
<ElRadioButton value="sim">网卡批量分配</ElRadioButton>
<ElRadioButton value="device">设备批量分配</ElRadioButton>
</ElRadioGroup>
</ElCard>
<ArtTableFullScreen>
<div class="asset-allocation-records-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<!-- 网卡分配 -->
<ElCard v-if="assignMode === 'sim'" shadow="never" style="margin-bottom: 20px">
<template #header>
<span style="font-weight: 500">选择网卡资产</span>
</template>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
/>
<ElRow :gutter="12" style="margin-bottom: 16px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="simSearchQuery" placeholder="ICCID/IMSI" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="simStatusFilter" placeholder="状态筛选" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="激活" value="active" />
<ElOption label="未激活" value="inactive" />
<ElOption label="停机" value="suspended" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElButton v-ripple @click="searchSims">搜索</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredSimData" index>
<template #default>
<ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="IMSI" prop="imsi" width="180" />
<ElTableColumn label="运营商" prop="operator" width="100">
<template #default="scope">
<ElTag size="small">{{ scope.row.operator }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'active'" type="success" size="small">激活</ElTag>
<ElTag v-else-if="scope.row.status === 'inactive'" type="info" size="small"
>未激活</ElTag
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="recordList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<ElTag v-else type="warning" size="small">停机</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="绑定设备" prop="deviceCode" width="150">
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
<ElTableColumn label="操作" width="120" fixed="right">
<template #default="scope">
<ElTag v-if="scope.row.deviceCode" type="primary" size="small">
{{ scope.row.deviceCode }}
</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
<ElButton type="primary" link @click="viewDetail(scope.row)">查看详情</ElButton>
</template>
</ElTableColumn>
<ElTableColumn label="剩余流量" prop="remainData" width="120" />
<ElTableColumn label="到期时间" prop="expireTime" width="180" />
</template>
</ArtTable>
</ElCard>
<!-- 设备分配 -->
<ElCard v-if="assignMode === 'device'" shadow="never">
<template #header>
<span style="font-weight: 500">选择设备资产</span>
</template>
<ElRow :gutter="12" style="margin-bottom: 16px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="deviceSearchQuery" placeholder="设备编号/名称" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="GPS定位器" value="gps" />
<ElOption label="智能水表" value="water_meter" />
<ElOption label="智能电表" value="electric_meter" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElButton v-ripple @click="searchDevices">搜索</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredDeviceData" index>
<template #default>
<ElTableColumn label="设备编号" prop="deviceCode" width="180" />
<ElTableColumn label="设备名称" prop="deviceName" min-width="180" />
<ElTableColumn label="设备类型" prop="deviceType" width="120">
<template #default="scope">
<ElTag size="small">{{ getDeviceTypeText(scope.row.deviceType) }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="绑定ICCID" prop="iccid" width="200">
<template #default="scope">
<ElTag v-if="scope.row.iccid" type="success" size="small">
{{ scope.row.iccid }}
</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
</template>
</ElTableColumn>
<ElTableColumn label="在线状态" prop="onlineStatus" width="100">
<template #default="scope">
<ElTag v-if="scope.row.onlineStatus === 'online'" type="success" size="small"
>在线</ElTag
>
<ElTag v-else type="info" size="small">离线</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
</template>
</ArtTable>
</ElCard>
<!-- 分配对话框 -->
<ElDialog v-model="assignDialogVisible" title="资产分配" width="600px" align-center>
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
<ElFormItem label="分配类型">
<ElTag v-if="assignForm.type === 'sim'" type="primary">网卡资产</ElTag>
<ElTag v-else type="success">设备资产</ElTag>
</ElFormItem>
<ElFormItem label="分配数量">
<div>
<span
v-if="assignForm.type === 'sim'"
style="font-size: 18px; font-weight: 600; color: var(--el-color-primary)"
>
{{ selectedSims.length }} 张网卡
</span>
<span
v-else
style="font-size: 18px; font-weight: 600; color: var(--el-color-success)"
>
{{ selectedDevices.length }} 个设备
</span>
</div>
</ElFormItem>
<ElFormItem label="目标代理商" prop="targetAgentId">
<ElSelect
v-model="assignForm.targetAgentId"
placeholder="请选择目标代理商"
filterable
style="width: 100%"
>
<ElOption
v-for="agent in agentList"
:key="agent.id"
:label="`${agent.agentName} (等级${agent.level})`"
:value="agent.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="分配说明" prop="remark">
<ElInput
v-model="assignForm.remark"
type="textarea"
:rows="3"
placeholder="请输入分配说明"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="assignDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
</div>
</template>
</ElDialog>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import type {
AssetAllocationRecord,
AllocationTypeEnum,
AssetTypeEnum
} from '@/types/api/card'
defineOptions({ name: 'AssetAssign' })
defineOptions({ name: 'AssetAllocationRecords' })
interface SimCard {
id: string
iccid: string
imsi: string
operator: string
status: string
deviceCode?: string
remainData: string
expireTime: string
const router = useRouter()
const loading = ref(false)
const tableRef = ref()
// 搜索表单初始值
const initialSearchState = {
allocation_type: undefined as AllocationTypeEnum | undefined,
asset_type: undefined as AssetTypeEnum | undefined,
asset_identifier: '',
allocation_no: '',
from_shop_id: undefined as number | undefined,
to_shop_id: undefined as number | undefined,
operator_id: undefined as number | undefined,
created_at_start: '',
created_at_end: ''
}
interface Device {
id: string
deviceCode: string
deviceName: string
deviceType: string
iccid?: string
onlineStatus: string
createTime: string
}
// 搜索表单
const formFilters = reactive({ ...initialSearchState })
const assignMode = ref('sim')
const simSearchQuery = ref('')
const simStatusFilter = ref('')
const deviceSearchQuery = ref('')
const deviceTypeFilter = ref('')
const assignDialogVisible = ref(false)
const formRef = ref<FormInstance>()
const selectedSims = ref<SimCard[]>([])
const selectedDevices = ref<Device[]>([])
const assignForm = reactive({
type: 'sim',
targetAgentId: '',
remark: ''
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
const assignRules = reactive<FormRules>({
targetAgentId: [{ required: true, message: '请选择目标代理商', trigger: 'change' }]
})
const agentList = ref([
{ id: '1', agentName: '华东区总代理', level: 1 },
{ id: '2', agentName: '华南区代理', level: 2 },
{ id: '3', agentName: '华北区代理', level: 1 }
])
const simMockData = ref<SimCard[]>([
// 搜索表单配置
const formItems: SearchFormItem[] = [
{
id: '1',
iccid: '89860123456789012345',
imsi: '460012345678901',
operator: '中国移动',
status: 'active',
deviceCode: 'DEV001',
remainData: '50GB',
expireTime: '2026-12-31'
label: '分配类型',
prop: 'allocation_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '分配', value: 'allocate' },
{ label: '回收', value: 'recall' }
]
},
{
id: '2',
iccid: '89860123456789012346',
imsi: '460012345678902',
operator: '中国联通',
status: 'active',
remainData: '80GB',
expireTime: '2026-11-30'
}
])
const deviceMockData = ref<Device[]>([
{
id: '1',
deviceCode: 'DEV001',
deviceName: 'GPS定位器-001',
deviceType: 'gps',
iccid: '89860123456789012345',
onlineStatus: 'online',
createTime: '2026-01-01 10:00:00'
label: '资产类型',
prop: 'asset_type',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '物联网卡', value: 'iot_card' },
{ label: '设备', value: 'device' }
]
},
{
id: '2',
deviceCode: 'DEV002',
deviceName: '智能水表-002',
deviceType: 'water_meter',
iccid: '89860123456789012346',
onlineStatus: 'offline',
createTime: '2026-01-02 11:00:00'
label: '分配单号',
prop: 'allocation_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入分配单号'
}
},
{
label: '资产标识符',
prop: 'asset_identifier',
type: 'input',
config: {
clearable: true,
placeholder: 'ICCID或设备号'
}
},
{
label: '创建时间',
prop: 'created_at_range',
type: 'date',
config: {
type: 'datetimerange',
clearable: true,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
]
// 列配置
const columnOptions = [
{ label: '分配单号', prop: 'allocation_no' },
{ label: '分配类型', prop: 'allocation_name' },
{ label: '资产类型', prop: 'asset_type_name' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '来源所有者', prop: 'from_owner_name' },
{ label: '目标所有者', prop: 'to_owner_name' },
{ label: '操作人', prop: 'operator_name' },
{ label: '关联卡数量', prop: 'related_card_count' },
{ label: '创建时间', prop: 'created_at' }
]
const recordList = ref<AssetAllocationRecord[]>([])
// 获取分配类型标签类型
const getAllocationTypeType = (type: AllocationTypeEnum) => {
return type === 'allocate' ? 'success' : 'warning'
}
// 获取资产类型标签类型
const getAssetTypeType = (type: AssetTypeEnum) => {
return type === 'iot_card' ? 'primary' : 'info'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'allocation_no',
label: '分配单号',
minWidth: 180
},
{
prop: 'allocation_name',
label: '分配类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
return h(
ElTag,
{ type: getAllocationTypeType(row.allocation_type) },
() => row.allocation_name
)
}
},
{
prop: 'asset_type_name',
label: '资产类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
return h(ElTag, { type: getAssetTypeType(row.asset_type) }, () => row.asset_type_name)
}
},
{
prop: 'asset_identifier',
label: '资产标识符',
minWidth: 180
},
{
prop: 'from_owner_name',
label: '来源所有者',
width: 150
},
{
prop: 'to_owner_name',
label: '目标所有者',
width: 150
},
{
prop: 'operator_name',
label: '操作人',
width: 120
},
{
prop: 'related_card_count',
label: '关联卡数量',
width: 120
},
{
prop: 'remark',
label: '备注',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: AssetAllocationRecord) => formatDateTime(row.created_at)
}
])
const filteredSimData = computed(() => {
let data = simMockData.value
if (simSearchQuery.value) {
data = data.filter(
(item) =>
item.iccid.includes(simSearchQuery.value) || item.imsi.includes(simSearchQuery.value)
)
}
if (simStatusFilter.value) {
data = data.filter((item) => item.status === simStatusFilter.value)
}
return data
onMounted(() => {
getTableData()
})
const filteredDeviceData = computed(() => {
let data = deviceMockData.value
if (deviceSearchQuery.value) {
data = data.filter(
(item) =>
item.deviceCode.includes(deviceSearchQuery.value) ||
item.deviceName.includes(deviceSearchQuery.value)
)
// 获取分配记录列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
...formFilters
}
if (deviceTypeFilter.value) {
data = data.filter((item) => item.deviceType === deviceTypeFilter.value)
// 处理日期范围
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) {
delete params[key]
}
return data
})
const getDeviceTypeText = (type: string) => {
const map: Record<string, string> = {
gps: 'GPS定位器',
water_meter: '智能水表',
electric_meter: '智能电表'
const res = await CardService.getAssetAllocationRecords(params)
if (res.code === 0) {
recordList.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取分配记录列表失败')
} finally {
loading.value = false
}
return map[type] || type
}
const searchSims = () => {}
const searchDevices = () => {}
const handleSimSelectionChange = (rows: SimCard[]) => {
selectedSims.value = rows
// 重置搜索
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.page = 1
getTableData()
}
const handleDeviceSelectionChange = (rows: Device[]) => {
selectedDevices.value = rows
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
const showAssignDialog = (type: string) => {
assignForm.type = type
assignDialogVisible.value = true
// 刷新表格
const handleRefresh = () => {
getTableData()
}
const handleAssignSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
assignDialogVisible.value = false
formRef.value.resetFields()
selectedSims.value = []
selectedDevices.value = []
ElMessage.success('资产分配成功')
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 查看详情
const viewDetail = (row: AssetAllocationRecord) => {
router.push({
path: '/asset-management/allocation-record-detail',
query: { id: row.id }
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-radio-button__inner) {
padding: 12px 20px;
}
.asset-allocation-records-page {
// Allocation records page styles
}
</style>

View File

@@ -0,0 +1,916 @@
<template>
<ArtTableFullScreen>
<div class="standalone-card-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showImportDialog">导入ICCID</ElButton>
<ElButton type="success" :disabled="selectedCards.length === 0" @click="showAllocateDialog">
批量分配
</ElButton>
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog">
批量回收
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="cardList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
>
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 导入ICCID对话框 -->
<ElDialog
v-model="importDialogVisible"
title="导入ICCID"
width="500px"
@close="handleImportDialogClose"
>
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
<ElFormItem label="运营商" prop="carrier_id">
<ElSelect v-model="importForm.carrier_id" placeholder="请选择运营商" style="width: 100%">
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem label="批次号" prop="batch_no">
<ElInput v-model="importForm.batch_no" placeholder="请输入批次号(可选)" />
</ElFormItem>
<ElFormItem label="上传文件" prop="file">
<ElUpload
ref="uploadRef"
class="upload-demo"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-exceed="handleExceed"
:file-list="fileList"
accept=".xlsx,.xls"
>
<template #trigger>
<ElButton type="primary">选择文件</ElButton>
</template>
<template #tip>
<div class="el-upload__tip">只能上传xlsx/xls文件且不超过10MB</div>
</template>
</ElUpload>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="importDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleImport" :loading="importLoading">
开始导入
</ElButton>
</div>
</template>
</ElDialog>
<!-- 批量分配对话框 -->
<ElDialog
v-model="allocateDialogVisible"
title="批量分配"
width="600px"
@close="handleAllocateDialogClose"
>
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
<ElFormItem label="目标店铺" prop="to_shop_id">
<ElSelect v-model="allocateForm.to_shop_id" placeholder="请选择目标店铺" style="width: 100%">
<ElOption label="店铺A" :value="1" />
<ElOption label="店铺B" :value="2" />
<ElOption label="店铺C" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="allocateForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'list'" label="ICCID列表">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
<ElSelect v-model="allocateForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
<ElSelect v-model="allocateForm.status" placeholder="请选择状态" clearable style="width: 100%">
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="批次号">
<ElInput v-model="allocateForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="allocateForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="allocateDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
确认分配
</ElButton>
</div>
</template>
</ElDialog>
<!-- 批量回收对话框 -->
<ElDialog
v-model="recallDialogVisible"
title="批量回收"
width="600px"
@close="handleRecallDialogClose"
>
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
<ElFormItem label="来源店铺" prop="from_shop_id">
<ElSelect v-model="recallForm.from_shop_id" placeholder="请选择来源店铺" style="width: 100%">
<ElOption label="店铺A" :value="1" />
<ElOption label="店铺B" :value="2" />
<ElOption label="店铺C" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="recallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'list'" label="ICCID列表">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
<ElSelect v-model="recallForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="批次号">
<ElInput v-model="recallForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="recallForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="recallDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleRecall" :loading="recallLoading">
确认回收
</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配结果对话框 -->
<ElDialog
v-model="resultDialogVisible"
:title="resultTitle"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="操作单号">{{ allocationResult.allocation_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="待处理总数">{{ allocationResult.total_count }}</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ allocationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ allocationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0" style="margin-top: 20px">
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="allocationResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="180" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="resultDialogVisible = false">确定</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { CardService } from '@/api/modules'
import { ElMessage, ElTag, ElUpload } from 'element-plus'
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import type {
StandaloneIotCard,
StandaloneCardStatus,
AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse
} from '@/types/api/card'
defineOptions({ name: 'StandaloneCardList' })
const loading = ref(false)
const importDialogVisible = ref(false)
const importLoading = ref(false)
const allocateDialogVisible = ref(false)
const allocateLoading = ref(false)
const recallDialogVisible = ref(false)
const recallLoading = ref(false)
const resultDialogVisible = ref(false)
const resultTitle = ref('')
const tableRef = ref()
const importFormRef = ref<FormInstance>()
const allocateFormRef = ref<FormInstance>()
const recallFormRef = ref<FormInstance>()
const uploadRef = ref()
const fileList = ref<UploadUserFile[]>([])
const selectedCards = ref<StandaloneIotCard[]>([])
const allocationResult = ref<AllocateStandaloneCardsResponse>({
allocation_no: '',
total_count: 0,
success_count: 0,
fail_count: 0,
failed_items: null
})
// 搜索表单初始值
const initialSearchState = {
status: undefined,
carrier_id: undefined,
shop_id: undefined,
iccid: '',
msisdn: '',
batch_no: '',
package_id: undefined,
is_distributed: undefined,
is_replaced: undefined,
iccid_start: '',
iccid_end: ''
}
// 搜索表单
const formFilters = reactive({ ...initialSearchState })
// 导入表单
const importForm = reactive({
carrier_id: undefined as number | undefined,
batch_no: '',
file: null as File | null
})
// 导入表单验证规则
const importRules = reactive<FormRules>({
carrier_id: [{ required: true, message: '请选择运营商', trigger: 'change' }],
file: [{ required: true, message: '请选择上传文件', trigger: 'change' }]
})
// 批量分配表单
const allocateForm = reactive<Partial<AllocateStandaloneCardsRequest>>({
selection_type: 'list',
to_shop_id: undefined,
iccids: [],
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
status: undefined,
batch_no: '',
remark: ''
})
// 批量分配表单验证规则
const allocateRules = reactive<FormRules>({
to_shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (rule, value, callback) => {
if (allocateForm.selection_type === 'range' && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (rule, value, callback) => {
if (allocateForm.selection_type === 'range' && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 批量回收表单
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
selection_type: 'list',
from_shop_id: undefined,
iccids: [],
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
batch_no: '',
remark: ''
})
// 批量回收表单验证规则
const recallRules = reactive<FormRules>({
from_shop_id: [{ required: true, message: '请选择来源店铺', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (rule, value, callback) => {
if (recallForm.selection_type === 'range' && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (rule, value, callback) => {
if (recallForm.selection_type === 'range' && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const formItems: SearchFormItem[] = [
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '在库', value: 1 },
{ label: '已分销', value: 2 },
{ label: '已激活', value: 3 },
{ label: '已停用', value: 4 }
]
},
{
label: '运营商',
prop: 'carrier_id',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 1 },
{ label: '中国联通', value: 2 },
{ label: '中国电信', value: 3 }
]
},
{
label: 'ICCID',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID'
}
},
{
label: '卡接入号',
prop: 'msisdn',
type: 'input',
config: {
clearable: true,
placeholder: '请输入卡接入号'
}
},
{
label: '批次号',
prop: 'batch_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入批次号'
}
},
{
label: '是否已分销',
prop: 'is_distributed',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
]
// 列配置
const columnOptions = [
{ label: 'ICCID', prop: 'iccid' },
{ label: 'IMSI', prop: 'imsi' },
{ label: '卡接入号', prop: 'msisdn' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '卡类型', prop: 'card_type' },
{ label: '状态', prop: 'status' },
{ label: '批次号', prop: 'batch_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '创建时间', prop: 'created_at' }
]
const cardList = ref<StandaloneIotCard[]>([])
// 获取状态标签类型
const getStatusType = (status: StandaloneCardStatus) => {
switch (status) {
case 1:
return 'info'
case 2:
return 'warning'
case 3:
return 'success'
case 4:
return 'danger'
default:
return 'info'
}
}
// 获取状态文本
const getStatusText = (status: StandaloneCardStatus) => {
switch (status) {
case 1:
return '在库'
case 2:
return '已分销'
case 3:
return '已激活'
case 4:
return '已停用'
default:
return '未知'
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'iccid',
label: 'ICCID',
minWidth: 180
},
{
prop: 'imsi',
label: 'IMSI',
width: 150
},
{
prop: 'msisdn',
label: '卡接入号',
width: 120
},
{
prop: 'carrier_name',
label: '运营商',
width: 100
},
{
prop: 'card_type',
label: '卡类型',
width: 100
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: StandaloneIotCard) => {
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
}
},
{
prop: 'batch_no',
label: '批次号',
width: 120
},
{
prop: 'shop_name',
label: '店铺名称',
width: 150
},
{
prop: 'activated_at',
label: '激活时间',
width: 160,
formatter: (row: StandaloneIotCard) => (row.activated_at ? formatDateTime(row.activated_at) : '-')
},
{
prop: 'created_at',
label: '创建时间',
width: 160,
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
}
])
onMounted(() => {
getTableData()
})
// 获取单卡列表
const getTableData = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
page_size: pagination.pageSize,
...formFilters
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await CardService.getStandaloneIotCards(params)
if (res.code === 0) {
cardList.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取单卡列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示导入对话框
const showImportDialog = () => {
importDialogVisible.value = true
importForm.carrier_id = undefined
importForm.batch_no = ''
importForm.file = null
fileList.value = []
if (importFormRef.value) {
importFormRef.value.resetFields()
}
}
// 文件变化处理
const handleFileChange: UploadProps['onChange'] = (file) => {
importForm.file = file.raw as File
if (importFormRef.value) {
importFormRef.value.clearValidate('file')
}
}
// 文件超出限制处理
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning('最多只能上传1个文件')
}
// 关闭导入对话框
const handleImportDialogClose = () => {
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
fileList.value = []
}
// 执行导入
const handleImport = async () => {
if (!importFormRef.value) return
await importFormRef.value.validate(async (valid) => {
if (valid) {
if (!importForm.file) {
ElMessage.warning('请选择上传文件')
return
}
importLoading.value = true
try {
const res = await CardService.importIotCards(
importForm.file,
importForm.carrier_id!,
importForm.batch_no || undefined
)
if (res.code === 0) {
ElMessage.success('导入任务已创建,请到任务管理页面查看导入进度')
importDialogVisible.value = false
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error('导入失败,请重试')
} finally {
importLoading.value = false
}
}
})
}
// 表格选择变化
const handleSelectionChange = (selection: StandaloneIotCard[]) => {
selectedCards.value = selection
}
// 显示批量分配对话框
const showAllocateDialog = () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要分配的卡')
return
}
allocateDialogVisible.value = true
Object.assign(allocateForm, {
selection_type: 'list',
to_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
status: undefined,
batch_no: '',
remark: ''
})
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 显示批量回收对话框
const showRecallDialog = () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要回收的卡')
return
}
recallDialogVisible.value = true
Object.assign(recallForm, {
selection_type: 'list',
from_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
batch_no: '',
remark: ''
})
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
// 关闭批量分配对话框
const handleAllocateDialogClose = () => {
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 关闭批量回收对话框
const handleRecallDialogClose = () => {
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
// 执行批量分配
const handleAllocate = async () => {
if (!allocateFormRef.value) return
await allocateFormRef.value.validate(async (valid) => {
if (valid) {
// 根据选卡方式构建请求参数
const params: Partial<AllocateStandaloneCardsRequest> = {
selection_type: allocateForm.selection_type!,
to_shop_id: allocateForm.to_shop_id!,
remark: allocateForm.remark
}
if (allocateForm.selection_type === 'list') {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要分配的卡')
return
}
} else if (allocateForm.selection_type === 'range') {
params.iccid_start = allocateForm.iccid_start
params.iccid_end = allocateForm.iccid_end
} else if (allocateForm.selection_type === 'filter') {
if (allocateForm.carrier_id) params.carrier_id = allocateForm.carrier_id
if (allocateForm.status) params.status = allocateForm.status
if (allocateForm.batch_no) params.batch_no = allocateForm.batch_no
}
allocateLoading.value = true
try {
const res = await CardService.allocateStandaloneCards(params)
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量分配结果'
allocateDialogVisible.value = false
resultDialogVisible.value = true
// 清空选择
if (tableRef.value) {
tableRef.value.clearSelection()
}
selectedCards.value = []
// 刷新列表
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error('批量分配失败,请重试')
} finally {
allocateLoading.value = false
}
}
})
}
// 执行批量回收
const handleRecall = async () => {
if (!recallFormRef.value) return
await recallFormRef.value.validate(async (valid) => {
if (valid) {
// 根据选卡方式构建请求参数
const params: Partial<RecallStandaloneCardsRequest> = {
selection_type: recallForm.selection_type!,
from_shop_id: recallForm.from_shop_id!,
remark: recallForm.remark
}
if (recallForm.selection_type === 'list') {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要回收的卡')
return
}
} else if (recallForm.selection_type === 'range') {
params.iccid_start = recallForm.iccid_start
params.iccid_end = recallForm.iccid_end
} else if (recallForm.selection_type === 'filter') {
if (recallForm.carrier_id) params.carrier_id = recallForm.carrier_id
if (recallForm.batch_no) params.batch_no = recallForm.batch_no
}
recallLoading.value = true
try {
const res = await CardService.recallStandaloneCards(params)
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量回收结果'
recallDialogVisible.value = false
resultDialogVisible.value = true
// 清空选择
if (tableRef.value) {
tableRef.value.clearSelection()
}
selectedCards.value = []
// 刷新列表
getTableData()
}
} catch (error) {
console.error(error)
ElMessage.error('批量回收失败,请重试')
} finally {
recallLoading.value = false
}
}
})
}
</script>
<style lang="scss" scoped>
.standalone-card-list-page {
// Card list page styles
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<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>
<!-- 任务基本信息 -->
<ElDescriptions title="任务基本信息" :column="3" border class="task-info">
<ElDescriptionsItem label="任务编号">{{ taskDetail?.task_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{ taskDetail?.batch_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">{{ taskDetail?.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>
<!-- 失败记录 -->
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
<ElDivider content-position="left">
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
</ElDivider>
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
<ElTableColumn prop="line" label="行号" width="100" />
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
</ElTable>
</div>
<!-- 跳过记录 -->
<div class="skipped-section" v-if="taskDetail?.skip_count && taskDetail.skip_count > 0">
<ElDivider content-position="left">
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
</ElDivider>
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
<ElTableColumn prop="line" label="行号" width="100" />
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
</ElTable>
</div>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage, ElTag, ElDescriptions, ElDescriptionsItem, ElDivider, ElTable, ElTableColumn } from 'element-plus'
import { formatDateTime } from '@/utils/business/format'
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
defineOptions({ name: 'TaskDetail' })
const router = useRouter()
const route = useRoute()
const taskDetail = ref<IotCardImportTaskDetail | null>(null)
const loading = ref(false)
// 获取状态标签类型
const getStatusType = (status?: IotCardImportTaskStatus) => {
if (!status) return 'info'
switch (status) {
case 1:
return 'info'
case 2:
return 'warning'
case 3:
return 'success'
case 4:
return 'danger'
default:
return 'info'
}
}
// 返回列表
const goBack = () => {
router.back()
}
// 获取任务详情
const getTaskDetail = async () => {
const taskId = route.query.id
if (!taskId) {
ElMessage.error('缺少任务ID参数')
goBack()
return
}
loading.value = true
try {
const res = await CardService.getIotCardImportTaskDetail(Number(taskId))
if (res.code === 0) {
taskDetail.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取任务详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
getTaskDetail()
})
</script>
<style lang="scss" scoped>
.task-detail-page {
.back-button-wrapper {
margin-bottom: 20px;
}
.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);
}
.failure-section,
.skipped-section {
margin-top: 20px;
.section-title {
font-size: 16px;
font-weight: bold;
}
}
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<ArtTableFullScreen>
<div class="task-management-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
/>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="taskList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
defineOptions({ name: 'TaskManagement' })
const router = useRouter()
const loading = ref(false)
const tableRef = ref()
// 搜索表单初始值
const initialSearchState = {
status: undefined,
carrier_id: undefined,
batch_no: '',
start_time: '',
end_time: ''
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '任务状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '待处理', value: 1 },
{ label: '处理中', value: 2 },
{ label: '已完成', value: 3 },
{ label: '失败', value: 4 }
]
},
{
label: '运营商',
prop: 'carrier_id',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 1 },
{ label: '中国联通', value: 2 },
{ label: '中国电信', value: 3 }
]
},
{
label: '批次号',
prop: 'batch_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入批次号'
}
},
{
label: '创建时间',
prop: 'dateRange',
type: 'daterange',
config: {
type: 'daterange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
]
// 列配置
const columnOptions = [
{ label: '任务编号', prop: 'task_no' },
{ label: '批次号', prop: 'batch_no' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '文件名', prop: 'file_name' },
{ label: '任务状态', prop: 'status' },
{ label: '总数', prop: 'total_count' },
{ label: '成功数', prop: 'success_count' },
{ label: '失败数', prop: 'fail_count' },
{ label: '跳过数', prop: 'skip_count' },
{ label: '创建时间', prop: 'created_at' },
{ label: '完成时间', prop: 'completed_at' },
{ label: '操作', prop: 'operation' }
]
const taskList = ref<IotCardImportTask[]>([])
// 获取状态标签类型
const getStatusType = (status: IotCardImportTaskStatus) => {
switch (status) {
case 1:
return 'info'
case 2:
return 'warning'
case 3:
return 'success'
case 4:
return 'danger'
default:
return 'info'
}
}
// 查看详情
const viewDetail = (row: IotCardImportTask) => {
router.push({
path: '/asset-management/task-detail',
query: { id: row.id }
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'task_no',
label: '任务编号',
width: 150
},
{
prop: 'batch_no',
label: '批次号',
width: 120
},
{
prop: 'carrier_name',
label: '运营商',
width: 100
},
{
prop: 'file_name',
label: '文件名',
minWidth: 200
},
{
prop: 'status',
label: '任务状态',
width: 100,
formatter: (row: IotCardImportTask) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
}
},
{
prop: 'total_count',
label: '总数',
width: 80
},
{
prop: 'success_count',
label: '成功数',
width: 80
},
{
prop: 'fail_count',
label: '失败数',
width: 80,
formatter: (row: IotCardImportTask) => {
const type = row.fail_count > 0 ? 'danger' : 'success'
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
}
},
{
prop: 'skip_count',
label: '跳过数',
width: 80
},
{
prop: 'created_at',
label: '创建时间',
width: 160,
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
},
{
prop: 'completed_at',
label: '完成时间',
width: 160,
formatter: (row: IotCardImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
},
{
prop: 'operation',
label: '操作',
width: 100,
fixed: 'right',
formatter: (row: IotCardImportTask) => {
return h(ArtButtonTable, {
type: 'view',
onClick: () => viewDetail(row)
})
}
}
])
onMounted(() => {
getTableData()
})
// 获取任务列表
const getTableData = async () => {
loading.value = true
try {
const params: any = {
page: pagination.page,
page_size: pagination.pageSize,
status: searchForm.status,
carrier_id: searchForm.carrier_id,
batch_no: searchForm.batch_no || undefined
}
// 处理时间范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
params.start_time = searchForm.dateRange[0]
params.end_time = searchForm.dateRange[1]
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await CardService.getIotCardImportTasks(params)
if (res.code === 0) {
taskList.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
ElMessage.error('获取任务列表失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
</script>
<style lang="scss" scoped>
.task-management-page {
// Task management page styles
}
</style>

View File

@@ -773,8 +773,9 @@
'div',
{
style:
'display: flex; justify-content: center; align-items: center; gap: 4px; cursor: pointer; color: var(--el-color-primary);',
onClick: (e: MouseEvent) => {
'display: flex; justify-content: center; align-items: center; gap: 4px; cursor: context-menu; color: var(--el-color-primary);',
onContextmenu: (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
handleOperationClick(row, e)
}