fetch(modify):修改原来的bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m53s

This commit is contained in:
sexygoat
2026-01-31 11:18:37 +08:00
parent 8a1388608c
commit 31440b2904
62 changed files with 3025 additions and 1421 deletions

View File

@@ -5,6 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -15,7 +16,13 @@
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
/>
>
<template #left>
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
批量导入IoT卡
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
@@ -36,25 +43,152 @@
</ArtTable>
</ElCard>
</div>
<!-- 导入对话框 -->
<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. 请先下载 CSV 模板文件按照模板格式填写IoT卡信息</p>
<p>2. 支持 CSV 格式.csv单次最多导入 1000 </p>
<p>3. CSV 文件编码UTF-8推荐 GBK</p>
<p>4. 必填字段iccidICCIDmsisdnMSISDN/手机号</p>
<p>5. 必须选择运营商</p>
</div>
</template>
</ElAlert>
<div style="margin-bottom: 20px">
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
下载导入模板
</ElButton>
</div>
<ElFormItem label="运营商" required style="margin-bottom: 20px">
<ElSelect v-model="selectedCarrierId" placeholder="请选择运营商" style="width: 100%">
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElUpload
ref="uploadRef"
drag
:auto-upload="false"
:on-change="handleFileChange"
:limit="1"
accept=".csv"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div>
<template #tip>
<div class="el-upload__tip">只能上传 CSV 文件且不超过 10MB</div>
</template>
</ElUpload>
<template #footer>
<ElButton @click="handleCancelImport">取消</ElButton>
<ElButton
type="primary"
:loading="uploading"
:disabled="!fileList.length || !selectedCarrierId"
@click="submitUpload"
>
开始导入
</ElButton>
</template>
</ElDialog>
<!-- 任务详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="IoT卡导入任务详情" width="900px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="任务编号" :span="2">{{
currentDetail.task_no
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusType(currentDetail.status)">{{ currentDetail.status_text }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">{{ currentDetail.carrier_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="文件名" :span="2">{{
currentDetail.file_name
}}</ElDescriptionsItem>
<ElDescriptionsItem label="总数">{{ currentDetail.total_count }}</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<span style="color: var(--el-color-success)">{{ currentDetail.success_count }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="跳过数">{{ currentDetail.skip_count }}</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<span style="color: var(--el-color-danger)">{{ currentDetail.fail_count }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="警告数">
<span style="color: var(--el-color-warning)">{{ currentDetail.warning_count }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">{{ currentDetail.started_at }}</ElDescriptionsItem>
<ElDescriptionsItem label="完成时间">{{ currentDetail.completed_at }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ currentDetail.created_at }}</ElDescriptionsItem>
<ElDescriptionsItem label="错误信息" :span="2">{{
currentDetail.error_message
}}</ElDescriptionsItem>
</ElDescriptions>
<ElDivider content-position="left">失败明细</ElDivider>
<div
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
style="max-height: 300px; overflow-y: auto"
>
<ElTable :data="currentDetail.failed_items" border size="small">
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
<ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="MSISDN" prop="msisdn" width="150" />
<ElTableColumn label="失败原因" prop="reason" min-width="200">
<template #default="{ row }">
{{ row.reason || row.error || '未知错误' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<ElEmpty v-else description="无失败记录" />
<template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
<ElButton
v-if="currentDetail.fail_count > 0"
type="primary"
:icon="Download"
@click="downloadFailData"
>
下载失败数据
</ElButton>
</template>
</ElDialog>
</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 { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } 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 { formatDateTime } from '@/utils/business/format'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { StorageService } from '@/api/modules/storage'
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
defineOptions({ name: 'IotCardTask' })
const router = useRouter()
const loading = ref(false)
const tableRef = ref()
const uploadRef = ref<UploadInstance>()
const fileList = ref<File[]>([])
const uploading = ref(false)
const importDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const selectedCarrierId = ref<number>()
// 搜索表单初始值
const initialSearchState = {
@@ -145,6 +279,7 @@
]
const taskList = ref<IotCardImportTask[]>([])
const currentDetail = ref<any>({})
// 获取状态标签类型
const getStatusType = (status: IotCardImportTaskStatus) => {
@@ -163,14 +298,24 @@
}
// 查看详情
const viewDetail = (row: IotCardImportTask) => {
router.push({
path: '/asset-management/task-detail',
query: {
id: row.id,
task_type: 'card'
const viewDetail = async (row: IotCardImportTask) => {
try {
const res = await CardService.getIotCardImportTaskDetail(row.id)
if (res.code === 0 && res.data) {
currentDetail.value = {
...res.data,
started_at: res.data.started_at ? formatDateTime(res.data.started_at) : '-',
completed_at: res.data.completed_at ? formatDateTime(res.data.completed_at) : '-',
created_at: res.data.created_at ? formatDateTime(res.data.created_at) : '-',
carrier_name: res.data.carrier_name || '-',
error_message: res.data.error_message || '-'
}
detailDialogVisible.value = true
}
})
} catch (error) {
console.error('获取任务详情失败:', error)
ElMessage.error('获取任务详情失败')
}
}
// 动态列配置
@@ -196,7 +341,8 @@
{
prop: 'file_name',
label: '文件名',
minWidth: 250
minWidth: 250,
showOverflowTooltip: true
},
{
prop: 'total_count',
@@ -206,15 +352,17 @@
{
prop: 'success_count',
label: '成功数',
width: 80
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) => {
const type = row.fail_count > 0 ? 'danger' : 'success'
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
}
},
{
@@ -225,37 +373,57 @@
{
prop: 'started_at',
label: '开始时间',
width: 160,
width: 180,
formatter: (row: IotCardImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
},
{
prop: 'completed_at',
label: '完成时间',
width: 160,
formatter: (row: IotCardImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
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: 160,
width: 180,
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
width: 180,
fixed: 'right',
formatter: (row: IotCardImportTask) => {
return h(ArtButtonTable, {
text: '查看详情',
onClick: () => viewDetail(row)
})
const buttons = []
// 显示"查看详情"按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => viewDetail(row)
})
)
// 如果有失败数据,显示"失败数据"按钮
if (row.fail_count > 0) {
buttons.push(
h(ArtButtonTable, {
text: '失败数据',
type: 'danger',
onClick: () => downloadFailDataByRow(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
@@ -330,10 +498,202 @@
pagination.page = newCurrentPage
getTableData()
}
// 从行数据下载失败数据
const downloadFailDataByRow = async (row: IotCardImportTask) => {
try {
const res = await CardService.getIotCardImportTaskDetail(row.id)
if (res.code === 0 && res.data) {
const detail = res.data
downloadFailDataFromDetail(detail, row.task_no)
}
} catch (error) {
console.error('下载失败数据失败:', error)
ElMessage.error('下载失败数据失败')
}
}
// 下载失败数据(从详情对话框)
const downloadFailData = () => {
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no)
}
// 下载失败数据的通用方法
const downloadFailDataFromDetail = (detail: any, taskNo: string) => {
const failReasons =
detail.failed_items?.map((item: any, index: number) => ({
row: index + 1,
iccid: item.iccid || '-',
msisdn: item.msisdn || '-',
message: item.reason || item.error || '未知错误'
})) || []
if (failReasons.length === 0) {
ElMessage.warning('没有失败数据可下载')
return
}
const headers = ['行号', 'ICCID', 'MSISDN', '失败原因']
const csvRows = [
headers.join(','),
...failReasons.map((item: any) =>
[item.row, 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卡导入失败数据_${taskNo}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('失败数据下载成功')
}
// 下载模板
const downloadTemplate = () => {
const csvContent = [
'iccid,msisdn',
'89860123456789012345,13800138000',
'89860123456789012346,13800138001',
'89860123456789012347,13800138002'
].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卡导入模板.csv')
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('IoT卡导入模板下载成功')
}
// 文件选择变化
const handleFileChange = (uploadFile: any) => {
const maxSize = 10 * 1024 * 1024
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
ElMessage.error('文件大小不能超过 10MB')
uploadRef.value?.clearFiles()
fileList.value = []
return
}
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
ElMessage.error('只能上传 CSV 文件')
uploadRef.value?.clearFiles()
fileList.value = []
return
}
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
}
// 清空文件
const clearFiles = () => {
uploadRef.value?.clearFiles()
fileList.value = []
selectedCarrierId.value = undefined
}
// 取消导入
const handleCancelImport = () => {
clearFiles()
importDialogVisible.value = false
}
// 提交上传
const submitUpload = async () => {
if (!selectedCarrierId.value) {
ElMessage.warning('请先选择运营商')
return
}
if (!fileList.value.length) {
ElMessage.warning('请先选择CSV文件')
return
}
const file = fileList.value[0]
uploading.value = true
try {
ElMessage.info('正在准备上传...')
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: 'text/csv',
purpose: 'iot_import'
})
if (uploadUrlRes.code !== 0) {
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
}
const { upload_url, file_key } = uploadUrlRes.data
ElMessage.info('正在上传文件...')
await StorageService.uploadFile(upload_url, file, 'text/csv')
ElMessage.info('正在创建导入任务...')
const importRes = await CardService.importIotCards({
carrier_id: selectedCarrierId.value,
file_key,
batch_no: `IOT-${Date.now()}`
})
if (importRes.code !== 0) {
throw new Error(importRes.msg || '创建导入任务失败')
}
const taskNo = importRes.data.task_no
handleCancelImport()
getTableData()
ElMessage.success({
message: `导入任务已创建!任务编号:${taskNo}`,
duration: 3000,
showClose: true
})
} catch (error: any) {
console.error('IoT卡导入失败:', error)
ElMessage.error(error.message || 'IoT卡导入失败')
} finally {
uploading.value = false
}
}
</script>
<style lang="scss" scoped>
.iot-card-task-page {
// IoT card task page styles
: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>