Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
546
src/views/batch/card-change-notice/index.vue
Normal file
546
src/views/batch/card-change-notice/index.vue
Normal file
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="通知标题/内容" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="待发送" value="pending" />
|
||||
<ElOption label="发送中" value="sending" />
|
||||
<ElOption label="已发送" value="sent" />
|
||||
<ElOption label="发送失败" value="failed" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="typeFilter" placeholder="通知类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="卡片更换" value="replace" />
|
||||
<ElOption label="卡片激活" value="activate" />
|
||||
<ElOption label="卡片停用" value="deactivate" />
|
||||
<ElOption label="套餐变更" value="plan_change" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showDialog('add')">新增通知</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="通知标题" prop="title" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="通知类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||
{{ getTypeText(scope.row.type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="目标用户数" prop="targetCount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.targetCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已发送" prop="sentCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.sentCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : scope.row.status === 'sent' ? 'success' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'pending'" type="info">待发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sending'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
发送中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送方式" prop="sendMethod" width="120">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; gap: 4px">
|
||||
<ElTag v-for="method in scope.row.sendMethods" :key="method" size="small">
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn label="发送时间" prop="sendTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.sendTime || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="240">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Promotion"
|
||||
@click="handleSend(scope.row)"
|
||||
>
|
||||
立即发送
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending' || scope.row.status === 'failed'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑通知对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增换卡通知' : '编辑换卡通知'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="通知标题" prop="title">
|
||||
<ElInput v-model="form.title" placeholder="请输入通知标题" maxlength="50" show-word-limit />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知类型" prop="type">
|
||||
<ElRadioGroup v-model="form.type">
|
||||
<ElRadio value="replace">卡片更换</ElRadio>
|
||||
<ElRadio value="activate">卡片激活</ElRadio>
|
||||
<ElRadio value="deactivate">卡片停用</ElRadio>
|
||||
<ElRadio value="plan_change">套餐变更</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知内容" prop="content">
|
||||
<ElInput
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入通知内容"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标用户" prop="targetType">
|
||||
<ElRadioGroup v-model="form.targetType">
|
||||
<ElRadio value="all">全部用户</ElRadio>
|
||||
<ElRadio value="specific">指定用户</ElRadio>
|
||||
<ElRadio value="batch">批量导入</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'specific'" label="用户列表">
|
||||
<ElSelect v-model="form.targetUsers" multiple placeholder="请选择目标用户" style="width: 100%">
|
||||
<ElOption label="张三 (13800138000)" value="user1" />
|
||||
<ElOption label="李四 (13900139000)" value="user2" />
|
||||
<ElOption label="王五 (13700137000)" value="user3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'batch'" label="用户文件">
|
||||
<ElUpload
|
||||
:action="uploadUrl"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx,.xls,.txt"
|
||||
>
|
||||
<ElButton type="primary">选择文件</ElButton>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 Excel 或 TXT 格式,每行一个用户手机号</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="发送方式" prop="sendMethods">
|
||||
<ElCheckboxGroup v-model="form.sendMethods">
|
||||
<ElCheckbox value="sms">短信</ElCheckbox>
|
||||
<ElCheckbox value="email">邮件</ElCheckbox>
|
||||
<ElCheckbox value="app">App推送</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="定时发送">
|
||||
<ElSwitch v-model="form.scheduleSend" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.scheduleSend ? '启用定时发送' : '立即发送' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.scheduleSend" label="发送时间" prop="scheduleTime">
|
||||
<ElDatePicker
|
||||
v-model="form.scheduleTime"
|
||||
type="datetime"
|
||||
placeholder="选择发送时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">
|
||||
{{ form.scheduleSend ? '创建定时任务' : '保存' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="通知详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="通知标题">{{ currentDetail.title }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知类型">
|
||||
<ElTag :type="getTypeTagType(currentDetail.type)">
|
||||
{{ getTypeText(currentDetail.type) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="目标用户数">{{ currentDetail.targetCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已发送数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.sentCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送状态">
|
||||
<ElTag v-if="currentDetail.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'sending'" type="warning">发送中</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
<ElTag v-else type="info">待发送</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送方式" :span="2">
|
||||
<ElTag v-for="method in currentDetail.sendMethods" :key="method" size="small" style="margin-right: 4px">
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送时间">{{ currentDetail.sendTime || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知内容" :span="2">
|
||||
{{ currentDetail.content }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Loading, Promotion } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CardChangeNotice' })
|
||||
|
||||
interface Notice {
|
||||
id: string
|
||||
title: string
|
||||
type: 'replace' | 'activate' | 'deactivate' | 'plan_change'
|
||||
content: string
|
||||
targetCount: number
|
||||
sentCount: number
|
||||
failCount: number
|
||||
progress: number
|
||||
status: 'pending' | 'sending' | 'sent' | 'failed'
|
||||
sendMethods: string[]
|
||||
createTime: string
|
||||
sendTime?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadUrl = ref('/api/batch/upload-users')
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [] as string[],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null as Date | null
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入通知标题', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择通知类型', trigger: 'change' }],
|
||||
content: [{ required: true, message: '请输入通知内容', trigger: 'blur' }],
|
||||
targetType: [{ required: true, message: '请选择目标用户类型', trigger: 'change' }],
|
||||
sendMethods: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请至少选择一种发送方式',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const mockData = ref<Notice[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'SIM卡更换通知-2026年1月批次',
|
||||
type: 'replace',
|
||||
content: '尊敬的用户,您的物联网卡将于近期进行更换,新卡将在3个工作日内寄出,请注意查收。',
|
||||
targetCount: 1000,
|
||||
sentCount: 980,
|
||||
failCount: 20,
|
||||
progress: 100,
|
||||
status: 'sent',
|
||||
sendMethods: ['sms', 'app'],
|
||||
createTime: '2026-01-09 09:00:00',
|
||||
sendTime: '2026-01-09 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '卡片激活成功通知',
|
||||
type: 'activate',
|
||||
content: '恭喜!您的物联网卡已成功激活,现在可以正常使用了。',
|
||||
targetCount: 500,
|
||||
sentCount: 350,
|
||||
failCount: 5,
|
||||
progress: 71,
|
||||
status: 'sending',
|
||||
sendMethods: ['sms', 'email'],
|
||||
createTime: '2026-01-09 11:30:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '套餐变更提醒',
|
||||
type: 'plan_change',
|
||||
content: '您好,您的套餐将于2026年2月1日起变更为新套餐,详情请登录系统查看。',
|
||||
targetCount: 800,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: ['sms'],
|
||||
createTime: '2026-01-09 14:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<Notice>({
|
||||
id: '',
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetCount: 0,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: [],
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.title.includes(searchQuery.value) || item.content.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
if (typeFilter.value) {
|
||||
data = data.filter((item) => item.type === typeFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
replace: '卡片更换',
|
||||
activate: '卡片激活',
|
||||
deactivate: '卡片停用',
|
||||
plan_change: '套餐变更'
|
||||
}
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const map: Record<string, any> = {
|
||||
replace: 'warning',
|
||||
activate: 'success',
|
||||
deactivate: 'danger',
|
||||
plan_change: 'primary'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getSendMethodText = (method: string) => {
|
||||
const map: Record<string, string> = {
|
||||
sms: '短信',
|
||||
email: '邮件',
|
||||
app: 'App'
|
||||
}
|
||||
return map[method] || method
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Notice) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
title: row.title,
|
||||
type: row.type,
|
||||
content: row.content,
|
||||
targetType: 'all',
|
||||
sendMethods: row.sendMethods,
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
title: form.title,
|
||||
type: form.type as any,
|
||||
content: form.content,
|
||||
targetCount: form.targetType === 'all' ? 1000 : form.targetUsers.length,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: form.sendMethods,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('通知创建成功')
|
||||
} else {
|
||||
ElMessage.success('通知更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = (row: Notice) => {
|
||||
ElMessageBox.confirm(`确定立即发送通知"${row.title}"吗?`, '发送确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = 'sending'
|
||||
ElMessage.success('通知发送中...')
|
||||
|
||||
// 模拟发送进度
|
||||
const timer = setInterval(() => {
|
||||
if (row.progress < 100) {
|
||||
row.progress += 10
|
||||
row.sentCount = Math.floor((row.targetCount * row.progress) / 100)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
row.status = 'sent'
|
||||
row.sendTime = new Date().toLocaleString('zh-CN')
|
||||
ElMessage.success('通知发送完成')
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Notice) => {
|
||||
ElMessageBox.confirm('确定删除该通知吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewDetail = (row: Notice) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多只能上传1个文件')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
480
src/views/batch/device-import/index.vue
Normal file
480
src/views/batch/device-import/index.vue
Normal file
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入设备</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 500 条</p>
|
||||
<p>3. 必填字段:设备编号、设备名称、设备类型、ICCID(绑定网卡)</p>
|
||||
<p>4. ICCID 必须在系统中已存在,否则导入失败</p>
|
||||
<p>5. 设备编号重复将自动跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">下载导入模板</ElButton>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-change="handleFileChange"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,且不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton type="success" :loading="uploading" :disabled="!fileList.length" @click="submitUpload">
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入统计 -->
|
||||
<ElRow :gutter="20" style="margin-top: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">今日导入</div>
|
||||
<div class="stat-value">1,250</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Upload /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功绑定</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">1,180</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"><SuccessFilled /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">导入失败</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">70</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><CircleCloseFilled /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">94.4%</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><TrendCharts /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<div>
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" style="width: 120px; margin-right: 12px" clearable>
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="处理中" value="processing" />
|
||||
<ElOption label="完成" value="success" />
|
||||
<ElOption label="失败" value="failed" />
|
||||
</ElSelect>
|
||||
<ElButton size="small" @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="filteredRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="设备总数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已绑定ICCID" prop="bindCount" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag type="success" size="small">{{ scope.row.bindCount }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功导入">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入失败">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定ICCID">
|
||||
<ElTag type="success">{{ currentDetail.bindCount }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length" style="max-height: 300px; overflow-y: auto">
|
||||
<ElTable :data="currentDetail.failReasons" border size="small">
|
||||
<ElTableColumn label="行号" prop="row" width="80" />
|
||||
<ElTableColumn label="设备编号" prop="deviceCode" width="150" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="失败原因" prop="message" min-width="200" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="无失败记录" />
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton v-if="currentDetail.failCount > 0" type="primary" :icon="Download" @click="downloadFailData(currentDetail)">
|
||||
下载失败数据
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Download,
|
||||
UploadFilled,
|
||||
View,
|
||||
Loading,
|
||||
Upload,
|
||||
SuccessFilled,
|
||||
CircleCloseFilled,
|
||||
TrendCharts
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadInstance, UploadRawFile } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'DeviceImport' })
|
||||
|
||||
interface FailReason {
|
||||
row: number
|
||||
deviceCode: string
|
||||
iccid: string
|
||||
message: string
|
||||
}
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
bindCount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
failReasons?: FailReason[]
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadUrl = ref('/api/batch/device-import')
|
||||
const fileList = ref<UploadRawFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusFilter = ref('')
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'DEV20260109001',
|
||||
fileName: '设备导入模板_20260109.xlsx',
|
||||
totalCount: 300,
|
||||
successCount: 285,
|
||||
failCount: 15,
|
||||
bindCount: 285,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 11:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, deviceCode: 'DEV001', iccid: '89860123456789012345', message: 'ICCID 不存在' },
|
||||
{ row: 23, deviceCode: 'DEV002', iccid: '89860123456789012346', message: '设备编号已存在' },
|
||||
{ row: 45, deviceCode: '', iccid: '89860123456789012347', message: '设备编号为空' },
|
||||
{ row: 67, deviceCode: 'DEV003', iccid: '', message: 'ICCID 为空' },
|
||||
{ row: 89, deviceCode: 'DEV004', iccid: '89860123456789012348', message: '设备类型不存在' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'DEV20260108001',
|
||||
fileName: '智能水表设备批量导入.xlsx',
|
||||
totalCount: 150,
|
||||
successCount: 150,
|
||||
failCount: 0,
|
||||
bindCount: 150,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 14:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'DEV20260107001',
|
||||
fileName: 'GPS定位器导入.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 180,
|
||||
failCount: 20,
|
||||
bindCount: 180,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 10:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{ row: 10, deviceCode: 'GPS001', iccid: '89860123456789012349', message: 'ICCID 已被其他设备绑定' },
|
||||
{ row: 20, deviceCode: 'GPS002', iccid: '89860123456789012350', message: 'ICCID 状态异常' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
bindCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
})
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
if (!statusFilter.value) return importRecords.value
|
||||
return importRecords.value.filter((item) => item.status === statusFilter.value)
|
||||
})
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any, files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
ElMessage.info('正在导入设备并绑定ICCID,请稍候...')
|
||||
|
||||
// 模拟上传和导入过程
|
||||
setTimeout(() => {
|
||||
const newRecord: ImportRecord = {
|
||||
id: Date.now().toString(),
|
||||
batchNo: `DEV${new Date().getTime()}`,
|
||||
fileName: fileList.value[0].name,
|
||||
totalCount: 100,
|
||||
successCount: 95,
|
||||
failCount: 5,
|
||||
bindCount: 95,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, deviceCode: 'TEST001', iccid: '89860123456789012351', message: 'ICCID 不存在' },
|
||||
{ row: 34, deviceCode: 'TEST002', iccid: '89860123456789012352', message: '设备类型无效' }
|
||||
]
|
||||
}
|
||||
|
||||
importRecords.value.unshift(newRecord)
|
||||
uploading.value = false
|
||||
clearFiles()
|
||||
ElMessage.success(
|
||||
`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount} 条,已绑定 ${newRecord.bindCount} 个ICCID`
|
||||
)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('失败数据下载完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
src/views/batch/sim-import/index.vue
Normal file
366
src/views/batch/sim-import/index.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入网卡</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写网卡信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 1000 条</p>
|
||||
<p>3. 必填字段:ICCID、运营商、套餐类型、流量规格</p>
|
||||
<p>4. 导入后系统将自动校验数据,重复 ICCID 将跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">下载导入模板</ElButton>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-change="handleFileChange"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,且不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton type="success" :loading="uploading" :disabled="!fileList.length" @click="submitUpload">
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<ElButton size="small" @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="importRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="总条数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
下载失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="导入详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总条数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount || 0 }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败原因" :span="2">
|
||||
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length">
|
||||
<div v-for="(reason, index) in currentDetail.failReasons" :key="index" style="margin-bottom: 4px">
|
||||
<ElTag type="danger" size="small">行{{ reason.row }}</ElTag>
|
||||
{{ reason.message }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">无</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance, UploadRawFile } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimImport' })
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
skipCount?: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
failReasons?: Array<{ row: number; message: string }>
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadUrl = ref('/api/batch/sim-import')
|
||||
const fileList = ref<UploadRawFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'IMP20260109001',
|
||||
fileName: '网卡导入模板_20260109.xlsx',
|
||||
totalCount: 500,
|
||||
successCount: 495,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 10:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 23, message: 'ICCID 格式错误' },
|
||||
{ row: 45, message: 'ICCID 已存在' },
|
||||
{ row: 67, message: '套餐类型不存在' },
|
||||
{ row: 89, message: '流量规格格式错误' },
|
||||
{ row: 123, message: '运营商代码无效' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'IMP20260108001',
|
||||
fileName: '网卡批量导入.xlsx',
|
||||
totalCount: 1000,
|
||||
successCount: 1000,
|
||||
failCount: 0,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 15:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'IMP20260107001',
|
||||
fileName: '测试数据.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 150,
|
||||
failCount: 50,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 09:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{ row: 10, message: 'ICCID 重复' },
|
||||
{ row: 20, message: '运营商字段为空' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
})
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
// 实际项目中应该调用下载接口
|
||||
setTimeout(() => {
|
||||
ElMessage.success('模板下载成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any, files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
ElMessage.info('正在导入,请稍候...')
|
||||
|
||||
// 模拟上传和导入过程
|
||||
setTimeout(() => {
|
||||
const newRecord: ImportRecord = {
|
||||
id: Date.now().toString(),
|
||||
batchNo: `IMP${new Date().getTime()}`,
|
||||
fileName: fileList.value[0].name,
|
||||
totalCount: 300,
|
||||
successCount: 295,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, message: 'ICCID 格式错误' },
|
||||
{ row: 34, message: 'ICCID 已存在' }
|
||||
]
|
||||
}
|
||||
|
||||
importRecords.value.unshift(newRecord)
|
||||
uploading.value = false
|
||||
clearFiles()
|
||||
ElMessage.success(`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount} 条`)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('下载完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user