872 lines
25 KiB
Vue
872 lines
25 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="iot-card-task-page" id="table-full-screen">
|
||
<!-- 搜索栏 -->
|
||
<ArtSearchBar
|
||
v-model:filter="searchForm"
|
||
:items="searchFormItems"
|
||
:show-expand="false"
|
||
@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"
|
||
:icon="Upload"
|
||
@click="importDialogVisible = true"
|
||
v-permission="'lot_task:bulk_import'"
|
||
>
|
||
批量导入IoT卡
|
||
</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<!-- 表格 -->
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="id"
|
||
:loading="loading"
|
||
:data="taskList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
:actions="getActions"
|
||
:actionsWidth="120"
|
||
@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>
|
||
|
||
<!-- 失败数据对话框 -->
|
||
<ElDialog
|
||
v-model="failDataDialogVisible"
|
||
title="失败数据详情"
|
||
width="900px"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<div v-if="failDataLoading" style="padding: 40px; text-align: center">
|
||
<ElSkeleton :rows="5" animated />
|
||
</div>
|
||
<div v-else-if="failedItems.length > 0">
|
||
<ElAlert type="warning" :closable="false" style="margin-bottom: 20px">
|
||
<template #title>
|
||
<div style="line-height: 1.8">
|
||
<p><strong>失败数据汇总:</strong></p>
|
||
<p>共 {{ failedItems.length }} 条记录导入失败</p>
|
||
</div>
|
||
</template>
|
||
</ElAlert>
|
||
|
||
<ElTable :data="failedItems" max-height="400" border>
|
||
<ElTableColumn prop="line" label="行号" width="80" />
|
||
<ElTableColumn prop="iccid" label="ICCID" width="200" show-overflow-tooltip />
|
||
<ElTableColumn prop="msisdn" label="MSISDN" width="150" show-overflow-tooltip />
|
||
<ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip />
|
||
</ElTable>
|
||
</div>
|
||
<div v-else style="padding: 40px; text-align: center">
|
||
<ElEmpty description="暂无失败数据" />
|
||
</div>
|
||
|
||
<template #footer>
|
||
<ElButton @click="failDataDialogVisible = false">关闭</ElButton>
|
||
<ElButton
|
||
v-if="failedItems.length > 0"
|
||
type="primary"
|
||
:icon="Download"
|
||
@click="downloadCurrentFailData"
|
||
>
|
||
下载失败数据
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 导入对话框 -->
|
||
<ElDialog v-model="importDialogVisible" title="批量导入IoT卡" width="700px" align-center>
|
||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||
<template #title>
|
||
<div style="line-height: 1.8">
|
||
<p><strong>导入说明:</strong></p>
|
||
<p>1. 请先下载 Excel 模板文件,按照模板格式填写IoT卡信息</p>
|
||
<p>2. 仅支持 Excel 格式(.xlsx),单次最多导入 1000 条</p>
|
||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||
<p>4. <strong>必填字段:ICCID、MSISDN(手机号)、virtual_no(虚拟号)</strong></p>
|
||
<p>5. <strong>Excel 文件必须包含 virtual_no 列,否则整批导入将失败</strong></p>
|
||
<p>6. virtual_no 为空将导入失败</p>
|
||
<p>7. 必须选择运营商</p>
|
||
</div>
|
||
</template>
|
||
</ElAlert>
|
||
|
||
<div style="margin-bottom: 20px">
|
||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
|
||
下载导入模板
|
||
</ElButton>
|
||
</div>
|
||
|
||
<ElRow :gutter="20">
|
||
<ElCol :span="12">
|
||
<ElFormItem label="运营商" required>
|
||
<ElSelect
|
||
v-model="selectedCarrierId"
|
||
placeholder="请输入运营商名称搜索"
|
||
style="width: 100%"
|
||
filterable
|
||
remote
|
||
:remote-method="handleCarrierSearch"
|
||
:loading="carrierLoading"
|
||
clearable
|
||
>
|
||
<ElOption
|
||
v-for="carrier in carrierList"
|
||
:key="carrier.id"
|
||
:label="carrier.carrier_name"
|
||
:value="carrier.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
<ElCol :span="12">
|
||
<ElFormItem label="卡业务类型">
|
||
<ElSelect
|
||
v-model="selectedCardCategory"
|
||
placeholder="请选择卡业务类型"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption label="普通卡" value="normal" />
|
||
<ElOption label="行业卡" value="industry" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
</ElRow>
|
||
|
||
<ElRow :gutter="20">
|
||
<ElCol :span="12">
|
||
<ElFormItem label="批次号" prop="batch_no">
|
||
<ElInput
|
||
v-model="importForm.batch_no"
|
||
placeholder="请输入批次号或点击生成"
|
||
:maxlength="100"
|
||
clearable
|
||
>
|
||
<template #append>
|
||
<ElButton @click="handleGenerateBatchNo">生成编码</ElButton>
|
||
</template>
|
||
</ElInput>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
<ElCol :span="12">
|
||
<ElFormItem label="实名认证策略">
|
||
<ElSelect
|
||
v-model="importForm.realname_policy"
|
||
placeholder="默认无需实名"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<ElOption label="无需实名" value="none" />
|
||
<ElOption label="先实名后充值/购买" value="before_order" />
|
||
<ElOption label="先充值/购买后实名" value="after_order" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
</ElRow>
|
||
|
||
<ElUpload
|
||
ref="uploadRef"
|
||
drag
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:limit="1"
|
||
accept=".xlsx"
|
||
>
|
||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||
<div class="el-upload__text">将 Excel 文件拖到此处,或<em>点击选择</em></div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">只能上传 .xlsx 格式的 Excel 文件,且不超过 300MB</div>
|
||
</template>
|
||
</ElUpload>
|
||
|
||
<template #footer>
|
||
<ElButton @click="handleCancelImport">取消</ElButton>
|
||
<ElButton
|
||
type="primary"
|
||
:loading="uploading"
|
||
:disabled="!fileList.length || !selectedCarrierId"
|
||
@click="submitUpload"
|
||
>
|
||
开始导入
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { h } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { CardService, CarrierService } from '@/api/modules'
|
||
import {
|
||
ElMessage,
|
||
ElTag,
|
||
ElFormItem,
|
||
ElSelect,
|
||
ElOption,
|
||
ElSkeleton,
|
||
ElEmpty
|
||
} from 'element-plus'
|
||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||
import type { UploadInstance } from 'element-plus'
|
||
import type { SearchFormItem } from '@/types'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import { StorageService } from '@/api/modules/storage'
|
||
import { generatePackageCode } from '@/utils/codeGenerator'
|
||
import { RoutesAlias } from '@/router/routesAlias'
|
||
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
||
import type { Carrier } from '@/types/api'
|
||
|
||
defineOptions({ name: 'IotCardTask' })
|
||
|
||
const router = useRouter()
|
||
|
||
const { hasAuth } = useAuth()
|
||
|
||
const loading = ref(false)
|
||
const tableRef = ref()
|
||
const uploadRef = ref<UploadInstance>()
|
||
const fileList = ref<File[]>([])
|
||
const uploading = ref(false)
|
||
const importDialogVisible = ref(false)
|
||
const selectedCarrierId = ref<number>()
|
||
const selectedCardCategory = ref<'normal' | 'industry'>('normal') // 默认普通卡
|
||
const carrierList = ref<Carrier[]>([])
|
||
const carrierLoading = ref(false)
|
||
const failDataDialogVisible = ref(false)
|
||
const failDataLoading = ref(false)
|
||
const failedItems = ref<any[]>([])
|
||
const currentFailTask = ref<IotCardImportTask | null>(null)
|
||
const importForm = reactive({
|
||
batch_no: '',
|
||
realname_policy: '' as '' | 'none' | 'before_order' | 'after_order'
|
||
})
|
||
|
||
// 搜索表单初始值
|
||
const initialSearchState = {
|
||
status: undefined,
|
||
carrier_id: undefined,
|
||
batch_no: '',
|
||
dateRange: undefined as any
|
||
}
|
||
|
||
// 搜索表单
|
||
const searchForm = reactive({ ...initialSearchState })
|
||
|
||
// 分页
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 搜索表单配置
|
||
const searchFormItems = computed<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',
|
||
options: carrierList.value.map((carrier: Carrier) => ({
|
||
label: carrier.carrier_name,
|
||
value: carrier.id
|
||
})),
|
||
config: {
|
||
clearable: true,
|
||
filterable: true,
|
||
remote: true,
|
||
remoteMethod: handleCarrierSearch,
|
||
placeholder: '请输入运营商名称搜索'
|
||
}
|
||
},
|
||
{
|
||
label: '批次号',
|
||
prop: 'batch_no',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入批次号'
|
||
}
|
||
},
|
||
{
|
||
label: '创建时间',
|
||
prop: 'dateRange',
|
||
type: 'daterange',
|
||
config: {
|
||
type: 'daterange',
|
||
startPlaceholder: '开始时间',
|
||
endPlaceholder: '结束时间',
|
||
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||
}
|
||
}
|
||
])
|
||
|
||
// 列配置
|
||
const columnOptions = [
|
||
{ label: '任务编号', prop: 'task_no' },
|
||
{ label: '任务状态', prop: 'status' },
|
||
{ label: '运营商', prop: 'carrier_name' },
|
||
{ label: '卡业务类型', prop: 'card_category' },
|
||
{ label: '文件名', prop: 'file_name' },
|
||
{ label: '总数', prop: 'total_count' },
|
||
{ label: '成功数', prop: 'success_count' },
|
||
{ label: '失败数', prop: 'fail_count' },
|
||
{ label: '跳过数', prop: 'skip_count' },
|
||
{ label: '开始时间', prop: 'started_at' },
|
||
{ label: '完成时间', prop: 'completed_at' },
|
||
{ label: '错误信息', prop: 'error_message' },
|
||
{ label: '创建时间', prop: 'created_at' }
|
||
]
|
||
|
||
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: RoutesAlias.TaskDetail,
|
||
query: {
|
||
id: row.id,
|
||
task_type: 'card'
|
||
}
|
||
})
|
||
}
|
||
|
||
// 处理名称点击
|
||
const handleNameClick = (row: IotCardImportTask) => {
|
||
if (hasAuth('iot_card_task:view_detail')) {
|
||
viewDetail(row)
|
||
} else {
|
||
ElMessage.warning('您没有查看详情的权限')
|
||
}
|
||
}
|
||
|
||
// 动态列配置
|
||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||
{
|
||
prop: 'task_no',
|
||
label: '任务编号',
|
||
width: 180,
|
||
formatter: (row: IotCardImportTask) => {
|
||
return h(
|
||
'span',
|
||
{
|
||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||
onClick: (e: MouseEvent) => {
|
||
e.stopPropagation()
|
||
handleNameClick(row)
|
||
}
|
||
},
|
||
row.task_no
|
||
)
|
||
}
|
||
},
|
||
{
|
||
prop: 'status',
|
||
label: '任务状态',
|
||
width: 100,
|
||
formatter: (row: IotCardImportTask) => {
|
||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||
}
|
||
},
|
||
{
|
||
prop: 'carrier_name',
|
||
label: '运营商',
|
||
width: 120,
|
||
showOverflowTooltip: true
|
||
},
|
||
{
|
||
prop: 'card_category',
|
||
label: '卡业务类型',
|
||
width: 110,
|
||
formatter: (row: IotCardImportTask) => {
|
||
if (!row.card_category) return '-'
|
||
return row.card_category === 'normal'
|
||
? '普通卡'
|
||
: row.card_category === 'industry'
|
||
? '行业卡'
|
||
: row.card_category
|
||
}
|
||
},
|
||
{
|
||
prop: 'file_name',
|
||
label: '文件名',
|
||
minWidth: 250,
|
||
showOverflowTooltip: true
|
||
},
|
||
{
|
||
prop: 'total_count',
|
||
label: '总数',
|
||
width: 80
|
||
},
|
||
{
|
||
prop: 'success_count',
|
||
label: '成功数',
|
||
width: 80,
|
||
formatter: (row: IotCardImportTask) => {
|
||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||
}
|
||
},
|
||
{
|
||
prop: 'fail_count',
|
||
label: '失败数',
|
||
width: 80,
|
||
formatter: (row: IotCardImportTask) => {
|
||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||
}
|
||
},
|
||
{
|
||
prop: 'skip_count',
|
||
label: '跳过数',
|
||
width: 80
|
||
},
|
||
{
|
||
prop: 'started_at',
|
||
label: '开始时间',
|
||
width: 180,
|
||
formatter: (row: IotCardImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
||
},
|
||
{
|
||
prop: 'completed_at',
|
||
label: '完成时间',
|
||
width: 180,
|
||
formatter: (row: IotCardImportTask) =>
|
||
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||
},
|
||
{
|
||
prop: 'error_message',
|
||
label: '错误信息',
|
||
minWidth: 200,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: IotCardImportTask) => row.error_message || '-'
|
||
},
|
||
{
|
||
prop: 'created_at',
|
||
label: '创建时间',
|
||
width: 180,
|
||
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
|
||
}
|
||
])
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
loadCarrierList()
|
||
})
|
||
|
||
// 获取IoT卡任务列表
|
||
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.items || []
|
||
pagination.total = res.data.total || 0
|
||
}
|
||
} catch (error) {
|
||
console.error(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()
|
||
}
|
||
|
||
// 显示失败数据弹窗
|
||
const showFailDataDialog = async (row: IotCardImportTask) => {
|
||
try {
|
||
failDataDialogVisible.value = true
|
||
failDataLoading.value = true
|
||
failedItems.value = []
|
||
currentFailTask.value = row
|
||
|
||
const res = await CardService.getIotCardImportTaskDetail(row.id)
|
||
if (res.code === 0 && res.data) {
|
||
const detail = res.data
|
||
failedItems.value =
|
||
detail.failed_items?.map((item: any) => ({
|
||
line: item.line || '-',
|
||
iccid: item.iccid || '-',
|
||
msisdn: item.msisdn || '-',
|
||
message: item.reason || item.error || '未知错误'
|
||
})) || []
|
||
|
||
if (failedItems.value.length === 0) {
|
||
ElMessage.warning('没有失败数据')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取失败数据失败:', error)
|
||
} finally {
|
||
failDataLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 下载当前失败数据
|
||
const downloadCurrentFailData = () => {
|
||
if (!currentFailTask.value || failedItems.value.length === 0) {
|
||
ElMessage.warning('没有失败数据可下载')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const headers = ['行号', 'ICCID', 'MSISDN', '失败原因']
|
||
const csvRows = [
|
||
headers.join(','),
|
||
...failedItems.value.map((item: any) =>
|
||
[item.line, item.iccid, item.msisdn, `"${item.message}"`].join(',')
|
||
)
|
||
]
|
||
const csvContent = csvRows.join('\n')
|
||
|
||
const BOM = '\uFEFF'
|
||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
|
||
|
||
const link = document.createElement('a')
|
||
const url = URL.createObjectURL(blob)
|
||
link.setAttribute('href', url)
|
||
link.setAttribute('download', `IoT卡导入失败数据_${currentFailTask.value.task_no}.csv`)
|
||
link.style.visibility = 'hidden'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
URL.revokeObjectURL(url)
|
||
|
||
ElMessage.success('失败数据下载成功')
|
||
} catch (error) {
|
||
console.error('下载失败数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 下载模板
|
||
const downloadTemplate = async () => {
|
||
try {
|
||
// 动态导入 xlsx 库
|
||
const XLSX = await import('xlsx')
|
||
|
||
// 创建示例数据(按固定列顺序:ICCID、MSISDN、虚拟号)
|
||
const templateData = [
|
||
{
|
||
ICCID: '89860123456789012345',
|
||
'MSISDN(接入号)': '13800138000',
|
||
虚拟号: 'V001'
|
||
},
|
||
{
|
||
ICCID: '89860123456789012346',
|
||
'MSISDN(接入号)': '13800138001',
|
||
虚拟号: 'V002'
|
||
},
|
||
{
|
||
ICCID: '89860123456789012347',
|
||
'MSISDN(接入号)': '13800138002',
|
||
虚拟号: 'V003'
|
||
}
|
||
]
|
||
|
||
// 创建工作簿
|
||
const wb = XLSX.utils.book_new()
|
||
const ws = XLSX.utils.json_to_sheet(templateData)
|
||
|
||
// 设置列宽
|
||
ws['!cols'] = [
|
||
{ wch: 25 }, // ICCID
|
||
{ wch: 20 }, // MSISDN(接入号)
|
||
{ wch: 15 } // 虚拟号
|
||
]
|
||
|
||
// 将所有单元格设置为文本格式,防止科学计数法
|
||
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1')
|
||
for (let R = range.s.r; R <= range.e.r; ++R) {
|
||
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
||
if (!ws[cellAddress]) continue
|
||
ws[cellAddress].t = 's' // 设置为字符串类型
|
||
}
|
||
}
|
||
|
||
// 添加工作表到工作簿
|
||
XLSX.utils.book_append_sheet(wb, ws, 'IoT卡导入模板')
|
||
|
||
// 导出文件
|
||
XLSX.writeFile(wb, 'IoT卡导入模板.xlsx')
|
||
|
||
ElMessage.success('IoT卡导入模板下载成功')
|
||
} catch (error) {
|
||
console.error('下载模板失败:', error)
|
||
}
|
||
}
|
||
|
||
// 文件选择变化
|
||
const handleFileChange = (uploadFile: any) => {
|
||
const maxSize = 300 * 1024 * 1024
|
||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||
ElMessage.error('文件大小不能超过 300MB')
|
||
uploadRef.value?.clearFiles()
|
||
fileList.value = []
|
||
return
|
||
}
|
||
|
||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.xlsx')) {
|
||
ElMessage.error('只能上传 .xlsx 格式的 Excel 文件')
|
||
uploadRef.value?.clearFiles()
|
||
fileList.value = []
|
||
return
|
||
}
|
||
|
||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||
}
|
||
|
||
// 清空文件
|
||
const clearFiles = () => {
|
||
uploadRef.value?.clearFiles()
|
||
fileList.value = []
|
||
selectedCarrierId.value = undefined
|
||
}
|
||
|
||
// 生成批次号
|
||
const handleGenerateBatchNo = () => {
|
||
const code = generatePackageCode().replace('PKG', 'IOT')
|
||
importForm.batch_no = code
|
||
ElMessage.success('批次号生成成功')
|
||
}
|
||
|
||
// 取消导入
|
||
const handleCancelImport = () => {
|
||
clearFiles()
|
||
selectedCardCategory.value = 'normal'
|
||
importForm.batch_no = ''
|
||
importForm.realname_policy = ''
|
||
importDialogVisible.value = false
|
||
}
|
||
|
||
// 加载运营商列表
|
||
const loadCarrierList = async (carrierName?: string) => {
|
||
carrierLoading.value = true
|
||
try {
|
||
const res = await CarrierService.getCarriers({
|
||
page: 1,
|
||
page_size: 20,
|
||
carrier_name: carrierName || undefined,
|
||
status: 1 // 只加载启用的运营商
|
||
})
|
||
if (res.code === 0) {
|
||
carrierList.value = res.data.items || []
|
||
}
|
||
} catch (error) {
|
||
console.error('获取运营商列表失败:', error)
|
||
} finally {
|
||
carrierLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 运营商搜索处理
|
||
const handleCarrierSearch = (query: string) => {
|
||
loadCarrierList(query)
|
||
}
|
||
|
||
// 提交上传
|
||
const submitUpload = async () => {
|
||
if (!selectedCarrierId.value) {
|
||
ElMessage.warning('请先选择运营商')
|
||
return
|
||
}
|
||
|
||
if (!fileList.value.length) {
|
||
ElMessage.warning('请先选择 Excel 文件')
|
||
return
|
||
}
|
||
|
||
const file = fileList.value[0]
|
||
uploading.value = true
|
||
|
||
try {
|
||
ElMessage.info('正在准备上传...')
|
||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||
file_name: file.name,
|
||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
purpose: 'iot_import'
|
||
})
|
||
|
||
if (uploadUrlRes.code !== 0) {
|
||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||
return
|
||
}
|
||
|
||
const { upload_url, file_key } = uploadUrlRes.data
|
||
|
||
ElMessage.info('正在上传文件...')
|
||
await StorageService.uploadFile(
|
||
upload_url,
|
||
file,
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
)
|
||
|
||
ElMessage.info('正在创建导入任务...')
|
||
const importRes = await CardService.importIotCards({
|
||
carrier_id: selectedCarrierId.value,
|
||
file_key,
|
||
batch_no: importForm.batch_no || undefined,
|
||
card_category: selectedCardCategory.value,
|
||
realname_policy: importForm.realname_policy || undefined
|
||
})
|
||
|
||
if (importRes.code !== 0) {
|
||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||
return
|
||
}
|
||
|
||
const taskNo = importRes.data.task_no
|
||
|
||
handleCancelImport()
|
||
await getTableData()
|
||
|
||
ElMessage.success({
|
||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||
duration: 3000,
|
||
showClose: true
|
||
})
|
||
} catch (error: any) {
|
||
console.error('IoT卡导入失败:', error)
|
||
console.log(error.message || 'IoT卡导入失败')
|
||
} finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取操作按钮
|
||
const getActions = (row: IotCardImportTask) => {
|
||
const actions: any[] = []
|
||
|
||
if (row.fail_count > 0 && hasAuth('iot_card_task:download_fail_data')) {
|
||
actions.push({
|
||
label: '失败数据',
|
||
handler: () => showFailDataDialog(row),
|
||
type: 'danger'
|
||
})
|
||
}
|
||
|
||
return actions
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.iot-card-task-page {
|
||
:deep(.el-icon--upload) {
|
||
margin-bottom: 16px;
|
||
font-size: 67px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
:deep(.el-upload__text) {
|
||
font-size: 14px;
|
||
color: var(--el-text-color-regular);
|
||
|
||
em {
|
||
font-style: normal;
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
}
|
||
</style>
|