fetch(add): 分配记录,批量分配/回收, 单卡列表, 任务列表, 导入ICCID
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m21s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m21s
This commit is contained in:
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,6 @@ export * from './commission'
|
||||
|
||||
// 通用状态相关
|
||||
export * from './status'
|
||||
|
||||
// IoT卡相关
|
||||
export * from './iotCard'
|
||||
|
||||
141
src/config/constants/iotCard.ts
Normal file
141
src/config/constants/iotCard.ts
Normal 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'
|
||||
}
|
||||
@@ -435,9 +435,11 @@
|
||||
"assetManagement": {
|
||||
"title": "资产管理",
|
||||
"singleCard": "单卡信息",
|
||||
"cardList": "网卡管理",
|
||||
"standaloneCardList": "单卡列表",
|
||||
"taskManagement": "任务管理",
|
||||
"taskDetail": "任务详情",
|
||||
"devices": "设备管理",
|
||||
"assetAssign": "资产分配",
|
||||
"assetAssign": "分配记录",
|
||||
"cardReplacementRequest": "换卡申请"
|
||||
},
|
||||
"account": {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', // 换卡申请
|
||||
|
||||
// 账户管理
|
||||
|
||||
@@ -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 // 起始ICCID(selection_type=range时必填)
|
||||
iccid_end?: string // 结束ICCID(selection_type=range时必填)
|
||||
carrier_id?: number // 运营商ID(selection_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 // 起始ICCID(selection_type=range时必填)
|
||||
iccid_end?: string // 结束ICCID(selection_type=range时必填)
|
||||
carrier_id?: number // 运营商ID(selection_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列表
|
||||
}
|
||||
|
||||
162
src/views/asset-management/allocation-record-detail/index.vue
Normal file
162
src/views/asset-management/allocation-record-detail/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
916
src/views/asset-management/card-list/index.vue
Normal file
916
src/views/asset-management/card-list/index.vue
Normal 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>
|
||||
178
src/views/asset-management/task-detail/index.vue
Normal file
178
src/views/asset-management/task-detail/index.vue
Normal 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>
|
||||
329
src/views/asset-management/task-management/index.vue
Normal file
329
src/views/asset-management/task-management/index.vue
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user