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:
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>
|
||||
Reference in New Issue
Block a user