| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- <template>
- <div class="ledger-import-page">
- <el-card class="page-header-card">
- <div class="page-title">台账一键导入</div>
- <div class="page-desc">支持12类台账Excel批量上传,请按模板格式准备文件后上传</div>
- </el-card>
- <!-- 一键全量导入 -->
- <el-card class="combined-import-card" shadow="never">
- <div class="combined-inner">
- <div class="combined-info">
- <el-icon class="combined-icon"><Files /></el-icon>
- <div>
- <div class="combined-title">一键全量导入</div>
- <div class="combined-desc">上传「旅检三部"三三"数字管理平台.xlsx」,系统将自动识别20个Sheet并分别导入对应台账表</div>
- </div>
- </div>
- <el-upload
- :action="''"
- :auto-upload="false"
- :show-file-list="false"
- :on-change="handleCombinedChange"
- accept=".xlsx,.xls"
- >
- <el-button type="primary" size="large" :loading="combinedLoading" class="combined-btn">
- <el-icon><Upload /></el-icon> 选择文件并全量导入
- </el-button>
- </el-upload>
- </div>
- <div v-if="combinedResult" class="combined-result">
- <div class="result-title">导入结果:</div>
- <el-tag
- v-for="(msg, sheet) in combinedResult" :key="sheet"
- :type="msg.includes('失败') || msg.includes('错误') ? 'danger' : 'success'"
- class="result-tag"
- >
- {{ sheet }}:{{ msg }}
- </el-tag>
- </div>
- </el-card>
- <!-- 按时间范围清理台账数据 -->
- <el-card class="clear-card" shadow="never">
- <div class="clear-inner">
- <div class="clear-info">
- <el-icon class="clear-icon"><DeleteFilled /></el-icon>
- <div>
- <div class="clear-title">清理台账数据</div>
- <div class="clear-desc">按导入时间范围删除全部20张台账表数据,同步清除台账来源的配分事项(手动录入不受影响)</div>
- </div>
- </div>
- <div class="clear-actions">
- <el-date-picker
- v-model="clearBeginDate"
- type="date"
- value-format="YYYY-MM-DD"
- placeholder="开始日期"
- style="width: 150px"
- />
- <span style="margin: 0 6px; color: #909399;">至</span>
- <el-date-picker
- v-model="clearEndDate"
- type="date"
- value-format="YYYY-MM-DD"
- placeholder="结束日期"
- :disabled-date="(d) => clearBeginDate && d < new Date(clearBeginDate)"
- style="width: 150px"
- />
- <el-button type="danger" :loading="clearLoading" :disabled="!clearBeginDate || !clearEndDate" @click="handleClear">
- <el-icon><Delete /></el-icon> 清理
- </el-button>
- </div>
- </div>
- <div v-if="clearResult" class="clear-result">
- <div class="result-title">清理结果(按导入时间 {{ clearBeginDate }} ~ {{ clearEndDate }}):</div>
- <div class="result-table">
- <span v-for="(count, name) in clearResult" :key="name" class="result-item">
- <el-tag :type="count > 0 ? 'warning' : 'info'" size="small">{{ name }}:{{ count }} 条</el-tag>
- </span>
- </div>
- </div>
- </el-card>
- <el-divider content-position="left">或按类型单独导入</el-divider>
- <div class="import-grid">
- <div v-for="item in importItems" :key="item.key" class="import-card">
- <el-card shadow="hover" :class="['ledger-card', item.status]">
- <div class="card-header">
- <el-icon class="card-icon"><component :is="item.icon" /></el-icon>
- <div class="card-title">{{ item.title }}</div>
- </div>
- <div class="card-desc">{{ item.desc }}</div>
- <div class="card-actions">
- <el-upload
- ref="uploadRefs"
- :action="''"
- :auto-upload="false"
- :show-file-list="false"
- :before-upload="(file) => beforeUpload(file, item)"
- :on-change="(file) => handleFileChange(file, item)"
- accept=".xlsx,.xls"
- >
- <el-button type="primary" size="small" :loading="item.loading">
- <el-icon><Upload /></el-icon> 选择文件上传
- </el-button>
- </el-upload>
- <el-button size="small" text @click="downloadTemplate(item)">
- <el-icon><Download /></el-icon> 下载模板
- </el-button>
- </div>
- <div v-if="item.lastResult" class="last-result" :class="item.lastResult.success ? 'success' : 'error'">
- <el-icon><component :is="item.lastResult.success ? 'CircleCheck' : 'CircleClose'" /></el-icon>
- {{ item.lastResult.msg }}
- </div>
- </el-card>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Upload, Download, Document, DocumentChecked, Warning, Trophy, UserFilled, Ticket, DataAnalysis, Histogram, Medal, Memo, Money, Calendar, Flag, Files, Reading, Management, FirstAidKit, House, Bell, Delete, DeleteFilled } from '@element-plus/icons-vue'
- import { importCombinedLedger, clearLedgerByTimeRange } from '@/api/ledger/index'
- import {
- importSupervisionProblem,
- importPatrolInspection,
- importRealtimeInterception,
- importServicePatrol,
- importComplaint,
- importSecurityTest,
- importChannelPassRate,
- importUnsafeEvent,
- importSeizureStats,
- importTerminalBonus,
- importExamScore,
- importRewardApproval,
- importDailyTraining,
- importLeaderDuty,
- importHealthSoldier,
- importDormFireSafety,
- importTrainingIssue
- } from '@/api/ledger/index'
- defineOptions({ name: 'LedgerImport' })
- // ── 一键全量导入 ──────────────────────────────────────
- const combinedLoading = ref(false)
- const combinedResult = ref(null)
- // ── 台账数据清理 ──────────────────────────────────────
- const clearBeginDate = ref('')
- const clearEndDate = ref('')
- const clearLoading = ref(false)
- const clearResult = ref(null)
- async function handleClear() {
- if (!clearBeginDate.value || !clearEndDate.value) {
- ElMessage.warning('请先选择清理时间范围')
- return
- }
- if (clearBeginDate.value > clearEndDate.value) {
- ElMessage.warning('开始日期不能晚于结束日期')
- return
- }
- const beginTime = clearBeginDate.value
- const endTime = clearEndDate.value
- try {
- await ElMessageBox.confirm(
- `确认删除导入时间在 ${beginTime} ~ ${endTime} 之间的全部台账数据?\n此操作不可恢复,请谨慎操作!`,
- '危险操作确认',
- { type: 'warning', confirmButtonText: '确认清理', cancelButtonText: '取消', confirmButtonClass: 'el-button--danger' }
- )
- } catch {
- return
- }
- clearLoading.value = true
- clearResult.value = null
- try {
- const res = await clearLedgerByTimeRange({ beginTime, endTime })
- if (res.code === 200) {
- clearResult.value = res.data || {}
- ElMessage.success(res.msg || '清理完成')
- } else {
- ElMessage.error(res.msg || '清理失败')
- }
- } catch (e) {
- ElMessage.error('清理失败,请查看后端日志')
- } finally {
- clearLoading.value = false
- }
- }
- async function handleCombinedChange(uploadFile) {
- const file = uploadFile.raw
- if (!file) return
- if (!beforeUpload(file)) return
- combinedLoading.value = true
- combinedResult.value = null
- const formData = new FormData()
- formData.append('file', file)
- try {
- const res = await importCombinedLedger(formData)
- if (res.code === 200) {
- combinedResult.value = res.data || {}
- ElMessage.success('全量导入完成,请查看各Sheet结果')
- } else {
- ElMessage.error(res.msg || '全量导入失败')
- }
- } catch (e) {
- ElMessage.error('全量导入失败,请查看后端日志')
- } finally {
- combinedLoading.value = false
- }
- }
- // ── 单类型导入 ──────────────────────────────────────
- const importItems = reactive([
- {
- key: 'supervisionProblem',
- title: '部门监察问题记录',
- desc: '记录监察问题、扣加分及评分维度',
- icon: 'Warning',
- api: importSupervisionProblem,
- loading: false,
- lastResult: null
- },
- {
- key: 'patrolInspection',
- title: '队室三级质控巡查记录',
- desc: '队室层级巡查问题记录',
- icon: 'DocumentChecked',
- api: importPatrolInspection,
- loading: false,
- lastResult: null
- },
- {
- key: 'realtimeInterception',
- title: '实时质控拦截记录',
- desc: '实时拦截物品及旅客记录',
- icon: 'Ticket',
- api: importRealtimeInterception,
- loading: false,
- lastResult: null
- },
- {
- key: 'servicePatrol',
- title: '服务巡查记录',
- desc: '服务质量巡查问题记录',
- icon: 'UserFilled',
- api: importServicePatrol,
- loading: false,
- lastResult: null
- },
- {
- key: 'complaint',
- title: '投诉情况记录',
- desc: '旅客投诉及处理结果',
- icon: 'Memo',
- api: importComplaint,
- loading: false,
- lastResult: null
- },
- {
- key: 'securityTest',
- title: '安保测试记录(部门)',
- desc: '安保测试项目及结果',
- icon: 'Flag',
- api: importSecurityTest,
- loading: false,
- lastResult: null
- },
- {
- key: 'channelPassRate',
- title: '通道过检率',
- desc: '各通道过检人数及过检率',
- icon: 'DataAnalysis',
- api: importChannelPassRate,
- loading: false,
- lastResult: null
- },
- {
- key: 'unsafeEvent',
- title: '不安全事件',
- desc: '安全事件记录及处理',
- icon: 'Document',
- api: importUnsafeEvent,
- loading: false,
- lastResult: null
- },
- {
- key: 'seizureStats',
- title: '2026查获违规品统计',
- desc: '查获违规品统计数据',
- icon: 'Histogram',
- api: importSeizureStats,
- loading: false,
- lastResult: null
- },
- {
- key: 'terminalBonus',
- title: '航站楼加分',
- desc: '航站楼相关加分记录',
- icon: 'Trophy',
- api: importTerminalBonus,
- loading: false,
- lastResult: null
- },
- {
- key: 'examScore',
- title: '成绩收集',
- desc: '考试成绩汇总导入',
- icon: 'Medal',
- api: importExamScore,
- loading: false,
- lastResult: null
- },
- {
- key: 'rewardApproval',
- title: '小额奖励审批单',
- desc: '小额奖励审批流程记录',
- icon: 'Money',
- api: importRewardApproval,
- loading: false,
- lastResult: null
- },
- {
- key: 'dailyTraining',
- title: '日常培训记录',
- desc: '月度培训任务及完成情况记录',
- icon: 'Reading',
- api: importDailyTraining,
- loading: false,
- lastResult: null
- },
- {
- key: 'leaderDuty',
- title: '组长履职情况记录',
- desc: '队室组长本班点评及问题处置',
- icon: 'Management',
- api: importLeaderDuty,
- loading: false,
- lastResult: null
- },
- {
- key: 'healthSoldier',
- title: '健康锐兵',
- desc: '员工身心健康状况台账记录',
- icon: 'FirstAidKit',
- api: importHealthSoldier,
- loading: false,
- lastResult: null
- },
- {
- key: 'dormFireSafety',
- title: '宿舍消防安全专项自查',
- desc: '宿舍消防隐患自查情况记录',
- icon: 'House',
- api: importDormFireSafety,
- loading: false,
- lastResult: null
- },
- {
- key: 'trainingIssue',
- title: '培训台账问题通报',
- desc: '培训台账发现问题及整改通报',
- icon: 'Bell',
- api: importTrainingIssue,
- loading: false,
- lastResult: null
- }
- ])
- function beforeUpload(file) {
- const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- || file.type === 'application/vnd.ms-excel'
- || file.name.endsWith('.xlsx')
- || file.name.endsWith('.xls')
- if (!isExcel) {
- ElMessage.error('只支持上传 .xlsx / .xls 格式文件')
- return false
- }
- if (file.size > 10 * 1024 * 1024) {
- ElMessage.error('文件大小不能超过 10MB')
- return false
- }
- return true
- }
- async function handleFileChange(uploadFile, item) {
- const file = uploadFile.raw
- if (!file) return
- if (!beforeUpload(file)) return
- item.loading = true
- item.lastResult = null
- const formData = new FormData()
- formData.append('file', file)
- try {
- const res = await item.api(formData)
- if (res.code === 200) {
- item.lastResult = { success: true, msg: res.msg || '导入成功' }
- ElMessage.success(item.title + ' - ' + (res.msg || '导入成功'))
- } else {
- item.lastResult = { success: false, msg: res.msg || '导入失败' }
- ElMessage.error(item.title + ' - ' + (res.msg || '导入失败'))
- }
- } catch (e) {
- item.lastResult = { success: false, msg: '网络异常,导入失败' }
- ElMessage.error(item.title + ' - 网络异常')
- } finally {
- item.loading = false
- }
- }
- function downloadTemplate(item) {
- ElMessage.info('模板文件功能开发中,请联系管理员获取模板')
- }
- </script>
- <style lang="scss" scoped>
- .ledger-import-page {
- padding: 20px;
- background: #f5f7fa;
- min-height: 100vh;
- }
- .page-header-card {
- margin-bottom: 20px;
- .page-title {
- font-size: 20px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 6px;
- }
- .page-desc {
- font-size: 13px;
- color: #909399;
- }
- }
- .combined-import-card {
- margin-bottom: 16px;
- .combined-inner {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- flex-wrap: wrap;
- }
- .combined-info {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- }
- .combined-icon {
- font-size: 36px;
- color: #409eff;
- flex-shrink: 0;
- margin-top: 2px;
- }
- .combined-title {
- font-size: 17px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 4px;
- }
- .combined-desc {
- font-size: 13px;
- color: #606266;
- max-width: 580px;
- }
- .combined-btn {
- min-width: 180px;
- height: 44px;
- font-size: 15px;
- }
- .combined-result {
- margin-top: 14px;
- padding-top: 14px;
- border-top: 1px dashed #dcdfe6;
- .result-title {
- font-size: 13px;
- color: #606266;
- margin-bottom: 8px;
- font-weight: 500;
- }
- .result-tag {
- margin-right: 8px;
- margin-bottom: 6px;
- }
- }
- }
- .clear-card {
- margin-bottom: 16px;
- border: 1px solid #fde2e2;
- background: #fff8f8;
- .clear-inner {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- flex-wrap: wrap;
- }
- .clear-info {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- }
- .clear-icon {
- font-size: 36px;
- color: #f56c6c;
- flex-shrink: 0;
- margin-top: 2px;
- }
- .clear-title {
- font-size: 17px;
- font-weight: 600;
- color: #f56c6c;
- margin-bottom: 4px;
- }
- .clear-desc {
- font-size: 13px;
- color: #909399;
- max-width: 520px;
- }
- .clear-actions {
- display: flex;
- gap: 10px;
- align-items: center;
- flex-wrap: wrap;
- }
- .clear-result {
- margin-top: 14px;
- padding-top: 14px;
- border-top: 1px dashed #fde2e2;
- .result-title {
- font-size: 13px;
- color: #606266;
- margin-bottom: 8px;
- font-weight: 500;
- }
- .result-table {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- }
- }
- }
- .import-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16px;
- }
- .ledger-card {
- height: 100%;
- :deep(.el-card__body) {
- padding: 16px;
- }
- .card-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- }
- .card-icon {
- font-size: 20px;
- color: #409eff;
- flex-shrink: 0;
- }
- .card-title {
- font-size: 15px;
- font-weight: 600;
- color: #303133;
- line-height: 1.3;
- }
- .card-desc {
- font-size: 12px;
- color: #909399;
- margin-bottom: 12px;
- min-height: 32px;
- }
- .card-actions {
- display: flex;
- gap: 8px;
- align-items: center;
- flex-wrap: wrap;
- }
- .last-result {
- margin-top: 10px;
- font-size: 12px;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 4px 8px;
- border-radius: 4px;
- &.success {
- background: #f0f9eb;
- color: #67c23a;
- }
- &.error {
- background: #fef0f0;
- color: #f56c6c;
- }
- }
- }
- @media (max-width: 768px) {
- .import-grid {
- grid-template-columns: 1fr;
- }
- }
- </style>
|