index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <template>
  2. <div class="ledger-import-page">
  3. <el-card class="page-header-card">
  4. <div class="page-title">台账一键导入</div>
  5. <div class="page-desc">支持12类台账Excel批量上传,请按模板格式准备文件后上传</div>
  6. </el-card>
  7. <!-- 一键全量导入 -->
  8. <el-card class="combined-import-card" shadow="never">
  9. <div class="combined-inner">
  10. <div class="combined-info">
  11. <el-icon class="combined-icon"><Files /></el-icon>
  12. <div>
  13. <div class="combined-title">一键全量导入</div>
  14. <div class="combined-desc">上传「旅检三部"三三"数字管理平台.xlsx」,系统将自动识别20个Sheet并分别导入对应台账表</div>
  15. </div>
  16. </div>
  17. <el-upload
  18. :action="''"
  19. :auto-upload="false"
  20. :show-file-list="false"
  21. :on-change="handleCombinedChange"
  22. accept=".xlsx,.xls"
  23. >
  24. <el-button type="primary" size="large" :loading="combinedLoading" class="combined-btn">
  25. <el-icon><Upload /></el-icon> 选择文件并全量导入
  26. </el-button>
  27. </el-upload>
  28. </div>
  29. <div v-if="combinedResult" class="combined-result">
  30. <div class="result-title">导入结果:</div>
  31. <el-tag
  32. v-for="(msg, sheet) in combinedResult" :key="sheet"
  33. :type="msg.includes('失败') || msg.includes('错误') ? 'danger' : 'success'"
  34. class="result-tag"
  35. >
  36. {{ sheet }}:{{ msg }}
  37. </el-tag>
  38. </div>
  39. </el-card>
  40. <!-- 按时间范围清理台账数据 -->
  41. <el-card class="clear-card" shadow="never">
  42. <div class="clear-inner">
  43. <div class="clear-info">
  44. <el-icon class="clear-icon"><DeleteFilled /></el-icon>
  45. <div>
  46. <div class="clear-title">清理台账数据</div>
  47. <div class="clear-desc">按导入时间范围删除全部20张台账表数据,同步清除台账来源的配分事项(手动录入不受影响)</div>
  48. </div>
  49. </div>
  50. <div class="clear-actions">
  51. <el-date-picker
  52. v-model="clearBeginDate"
  53. type="date"
  54. value-format="YYYY-MM-DD"
  55. placeholder="开始日期"
  56. style="width: 150px"
  57. />
  58. <span style="margin: 0 6px; color: #909399;">至</span>
  59. <el-date-picker
  60. v-model="clearEndDate"
  61. type="date"
  62. value-format="YYYY-MM-DD"
  63. placeholder="结束日期"
  64. :disabled-date="(d) => clearBeginDate && d < new Date(clearBeginDate)"
  65. style="width: 150px"
  66. />
  67. <el-button type="danger" :loading="clearLoading" :disabled="!clearBeginDate || !clearEndDate" @click="handleClear">
  68. <el-icon><Delete /></el-icon> 清理
  69. </el-button>
  70. </div>
  71. </div>
  72. <div v-if="clearResult" class="clear-result">
  73. <div class="result-title">清理结果(按导入时间 {{ clearBeginDate }} ~ {{ clearEndDate }}):</div>
  74. <div class="result-table">
  75. <span v-for="(count, name) in clearResult" :key="name" class="result-item">
  76. <el-tag :type="count > 0 ? 'warning' : 'info'" size="small">{{ name }}:{{ count }} 条</el-tag>
  77. </span>
  78. </div>
  79. </div>
  80. </el-card>
  81. <el-divider content-position="left">或按类型单独导入</el-divider>
  82. <div class="import-grid">
  83. <div v-for="item in importItems" :key="item.key" class="import-card">
  84. <el-card shadow="hover" :class="['ledger-card', item.status]">
  85. <div class="card-header">
  86. <el-icon class="card-icon"><component :is="item.icon" /></el-icon>
  87. <div class="card-title">{{ item.title }}</div>
  88. </div>
  89. <div class="card-desc">{{ item.desc }}</div>
  90. <div class="card-actions">
  91. <el-upload
  92. ref="uploadRefs"
  93. :action="''"
  94. :auto-upload="false"
  95. :show-file-list="false"
  96. :before-upload="(file) => beforeUpload(file, item)"
  97. :on-change="(file) => handleFileChange(file, item)"
  98. accept=".xlsx,.xls"
  99. >
  100. <el-button type="primary" size="small" :loading="item.loading">
  101. <el-icon><Upload /></el-icon> 选择文件上传
  102. </el-button>
  103. </el-upload>
  104. <el-button size="small" text @click="downloadTemplate(item)">
  105. <el-icon><Download /></el-icon> 下载模板
  106. </el-button>
  107. </div>
  108. <div v-if="item.lastResult" class="last-result" :class="item.lastResult.success ? 'success' : 'error'">
  109. <el-icon><component :is="item.lastResult.success ? 'CircleCheck' : 'CircleClose'" /></el-icon>
  110. {{ item.lastResult.msg }}
  111. </div>
  112. </el-card>
  113. </div>
  114. </div>
  115. </div>
  116. </template>
  117. <script setup>
  118. import { ref, reactive } from 'vue'
  119. import { ElMessage, ElMessageBox } from 'element-plus'
  120. 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'
  121. import { importCombinedLedger, clearLedgerByTimeRange } from '@/api/ledger/index'
  122. import {
  123. importSupervisionProblem,
  124. importPatrolInspection,
  125. importRealtimeInterception,
  126. importServicePatrol,
  127. importComplaint,
  128. importSecurityTest,
  129. importChannelPassRate,
  130. importUnsafeEvent,
  131. importSeizureStats,
  132. importTerminalBonus,
  133. importExamScore,
  134. importRewardApproval,
  135. importDailyTraining,
  136. importLeaderDuty,
  137. importHealthSoldier,
  138. importDormFireSafety,
  139. importTrainingIssue
  140. } from '@/api/ledger/index'
  141. defineOptions({ name: 'LedgerImport' })
  142. // ── 一键全量导入 ──────────────────────────────────────
  143. const combinedLoading = ref(false)
  144. const combinedResult = ref(null)
  145. // ── 台账数据清理 ──────────────────────────────────────
  146. const clearBeginDate = ref('')
  147. const clearEndDate = ref('')
  148. const clearLoading = ref(false)
  149. const clearResult = ref(null)
  150. async function handleClear() {
  151. if (!clearBeginDate.value || !clearEndDate.value) {
  152. ElMessage.warning('请先选择清理时间范围')
  153. return
  154. }
  155. if (clearBeginDate.value > clearEndDate.value) {
  156. ElMessage.warning('开始日期不能晚于结束日期')
  157. return
  158. }
  159. const beginTime = clearBeginDate.value
  160. const endTime = clearEndDate.value
  161. try {
  162. await ElMessageBox.confirm(
  163. `确认删除导入时间在 ${beginTime} ~ ${endTime} 之间的全部台账数据?\n此操作不可恢复,请谨慎操作!`,
  164. '危险操作确认',
  165. { type: 'warning', confirmButtonText: '确认清理', cancelButtonText: '取消', confirmButtonClass: 'el-button--danger' }
  166. )
  167. } catch {
  168. return
  169. }
  170. clearLoading.value = true
  171. clearResult.value = null
  172. try {
  173. const res = await clearLedgerByTimeRange({ beginTime, endTime })
  174. if (res.code === 200) {
  175. clearResult.value = res.data || {}
  176. ElMessage.success(res.msg || '清理完成')
  177. } else {
  178. ElMessage.error(res.msg || '清理失败')
  179. }
  180. } catch (e) {
  181. ElMessage.error('清理失败,请查看后端日志')
  182. } finally {
  183. clearLoading.value = false
  184. }
  185. }
  186. async function handleCombinedChange(uploadFile) {
  187. const file = uploadFile.raw
  188. if (!file) return
  189. if (!beforeUpload(file)) return
  190. combinedLoading.value = true
  191. combinedResult.value = null
  192. const formData = new FormData()
  193. formData.append('file', file)
  194. try {
  195. const res = await importCombinedLedger(formData)
  196. if (res.code === 200) {
  197. combinedResult.value = res.data || {}
  198. ElMessage.success('全量导入完成,请查看各Sheet结果')
  199. } else {
  200. ElMessage.error(res.msg || '全量导入失败')
  201. }
  202. } catch (e) {
  203. ElMessage.error('全量导入失败,请查看后端日志')
  204. } finally {
  205. combinedLoading.value = false
  206. }
  207. }
  208. // ── 单类型导入 ──────────────────────────────────────
  209. const importItems = reactive([
  210. {
  211. key: 'supervisionProblem',
  212. title: '部门监察问题记录',
  213. desc: '记录监察问题、扣加分及评分维度',
  214. icon: 'Warning',
  215. api: importSupervisionProblem,
  216. loading: false,
  217. lastResult: null
  218. },
  219. {
  220. key: 'patrolInspection',
  221. title: '队室三级质控巡查记录',
  222. desc: '队室层级巡查问题记录',
  223. icon: 'DocumentChecked',
  224. api: importPatrolInspection,
  225. loading: false,
  226. lastResult: null
  227. },
  228. {
  229. key: 'realtimeInterception',
  230. title: '实时质控拦截记录',
  231. desc: '实时拦截物品及旅客记录',
  232. icon: 'Ticket',
  233. api: importRealtimeInterception,
  234. loading: false,
  235. lastResult: null
  236. },
  237. {
  238. key: 'servicePatrol',
  239. title: '服务巡查记录',
  240. desc: '服务质量巡查问题记录',
  241. icon: 'UserFilled',
  242. api: importServicePatrol,
  243. loading: false,
  244. lastResult: null
  245. },
  246. {
  247. key: 'complaint',
  248. title: '投诉情况记录',
  249. desc: '旅客投诉及处理结果',
  250. icon: 'Memo',
  251. api: importComplaint,
  252. loading: false,
  253. lastResult: null
  254. },
  255. {
  256. key: 'securityTest',
  257. title: '安保测试记录(部门)',
  258. desc: '安保测试项目及结果',
  259. icon: 'Flag',
  260. api: importSecurityTest,
  261. loading: false,
  262. lastResult: null
  263. },
  264. {
  265. key: 'channelPassRate',
  266. title: '通道过检率',
  267. desc: '各通道过检人数及过检率',
  268. icon: 'DataAnalysis',
  269. api: importChannelPassRate,
  270. loading: false,
  271. lastResult: null
  272. },
  273. {
  274. key: 'unsafeEvent',
  275. title: '不安全事件',
  276. desc: '安全事件记录及处理',
  277. icon: 'Document',
  278. api: importUnsafeEvent,
  279. loading: false,
  280. lastResult: null
  281. },
  282. {
  283. key: 'seizureStats',
  284. title: '2026查获违规品统计',
  285. desc: '查获违规品统计数据',
  286. icon: 'Histogram',
  287. api: importSeizureStats,
  288. loading: false,
  289. lastResult: null
  290. },
  291. {
  292. key: 'terminalBonus',
  293. title: '航站楼加分',
  294. desc: '航站楼相关加分记录',
  295. icon: 'Trophy',
  296. api: importTerminalBonus,
  297. loading: false,
  298. lastResult: null
  299. },
  300. {
  301. key: 'examScore',
  302. title: '成绩收集',
  303. desc: '考试成绩汇总导入',
  304. icon: 'Medal',
  305. api: importExamScore,
  306. loading: false,
  307. lastResult: null
  308. },
  309. {
  310. key: 'rewardApproval',
  311. title: '小额奖励审批单',
  312. desc: '小额奖励审批流程记录',
  313. icon: 'Money',
  314. api: importRewardApproval,
  315. loading: false,
  316. lastResult: null
  317. },
  318. {
  319. key: 'dailyTraining',
  320. title: '日常培训记录',
  321. desc: '月度培训任务及完成情况记录',
  322. icon: 'Reading',
  323. api: importDailyTraining,
  324. loading: false,
  325. lastResult: null
  326. },
  327. {
  328. key: 'leaderDuty',
  329. title: '组长履职情况记录',
  330. desc: '队室组长本班点评及问题处置',
  331. icon: 'Management',
  332. api: importLeaderDuty,
  333. loading: false,
  334. lastResult: null
  335. },
  336. {
  337. key: 'healthSoldier',
  338. title: '健康锐兵',
  339. desc: '员工身心健康状况台账记录',
  340. icon: 'FirstAidKit',
  341. api: importHealthSoldier,
  342. loading: false,
  343. lastResult: null
  344. },
  345. {
  346. key: 'dormFireSafety',
  347. title: '宿舍消防安全专项自查',
  348. desc: '宿舍消防隐患自查情况记录',
  349. icon: 'House',
  350. api: importDormFireSafety,
  351. loading: false,
  352. lastResult: null
  353. },
  354. {
  355. key: 'trainingIssue',
  356. title: '培训台账问题通报',
  357. desc: '培训台账发现问题及整改通报',
  358. icon: 'Bell',
  359. api: importTrainingIssue,
  360. loading: false,
  361. lastResult: null
  362. }
  363. ])
  364. function beforeUpload(file) {
  365. const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  366. || file.type === 'application/vnd.ms-excel'
  367. || file.name.endsWith('.xlsx')
  368. || file.name.endsWith('.xls')
  369. if (!isExcel) {
  370. ElMessage.error('只支持上传 .xlsx / .xls 格式文件')
  371. return false
  372. }
  373. if (file.size > 10 * 1024 * 1024) {
  374. ElMessage.error('文件大小不能超过 10MB')
  375. return false
  376. }
  377. return true
  378. }
  379. async function handleFileChange(uploadFile, item) {
  380. const file = uploadFile.raw
  381. if (!file) return
  382. if (!beforeUpload(file)) return
  383. item.loading = true
  384. item.lastResult = null
  385. const formData = new FormData()
  386. formData.append('file', file)
  387. try {
  388. const res = await item.api(formData)
  389. if (res.code === 200) {
  390. item.lastResult = { success: true, msg: res.msg || '导入成功' }
  391. ElMessage.success(item.title + ' - ' + (res.msg || '导入成功'))
  392. } else {
  393. item.lastResult = { success: false, msg: res.msg || '导入失败' }
  394. ElMessage.error(item.title + ' - ' + (res.msg || '导入失败'))
  395. }
  396. } catch (e) {
  397. item.lastResult = { success: false, msg: '网络异常,导入失败' }
  398. ElMessage.error(item.title + ' - 网络异常')
  399. } finally {
  400. item.loading = false
  401. }
  402. }
  403. function downloadTemplate(item) {
  404. ElMessage.info('模板文件功能开发中,请联系管理员获取模板')
  405. }
  406. </script>
  407. <style lang="scss" scoped>
  408. .ledger-import-page {
  409. padding: 20px;
  410. background: #f5f7fa;
  411. min-height: 100vh;
  412. }
  413. .page-header-card {
  414. margin-bottom: 20px;
  415. .page-title {
  416. font-size: 20px;
  417. font-weight: 600;
  418. color: #303133;
  419. margin-bottom: 6px;
  420. }
  421. .page-desc {
  422. font-size: 13px;
  423. color: #909399;
  424. }
  425. }
  426. .combined-import-card {
  427. margin-bottom: 16px;
  428. .combined-inner {
  429. display: flex;
  430. align-items: center;
  431. justify-content: space-between;
  432. gap: 16px;
  433. flex-wrap: wrap;
  434. }
  435. .combined-info {
  436. display: flex;
  437. align-items: flex-start;
  438. gap: 12px;
  439. }
  440. .combined-icon {
  441. font-size: 36px;
  442. color: #409eff;
  443. flex-shrink: 0;
  444. margin-top: 2px;
  445. }
  446. .combined-title {
  447. font-size: 17px;
  448. font-weight: 600;
  449. color: #303133;
  450. margin-bottom: 4px;
  451. }
  452. .combined-desc {
  453. font-size: 13px;
  454. color: #606266;
  455. max-width: 580px;
  456. }
  457. .combined-btn {
  458. min-width: 180px;
  459. height: 44px;
  460. font-size: 15px;
  461. }
  462. .combined-result {
  463. margin-top: 14px;
  464. padding-top: 14px;
  465. border-top: 1px dashed #dcdfe6;
  466. .result-title {
  467. font-size: 13px;
  468. color: #606266;
  469. margin-bottom: 8px;
  470. font-weight: 500;
  471. }
  472. .result-tag {
  473. margin-right: 8px;
  474. margin-bottom: 6px;
  475. }
  476. }
  477. }
  478. .clear-card {
  479. margin-bottom: 16px;
  480. border: 1px solid #fde2e2;
  481. background: #fff8f8;
  482. .clear-inner {
  483. display: flex;
  484. align-items: center;
  485. justify-content: space-between;
  486. gap: 16px;
  487. flex-wrap: wrap;
  488. }
  489. .clear-info {
  490. display: flex;
  491. align-items: flex-start;
  492. gap: 12px;
  493. }
  494. .clear-icon {
  495. font-size: 36px;
  496. color: #f56c6c;
  497. flex-shrink: 0;
  498. margin-top: 2px;
  499. }
  500. .clear-title {
  501. font-size: 17px;
  502. font-weight: 600;
  503. color: #f56c6c;
  504. margin-bottom: 4px;
  505. }
  506. .clear-desc {
  507. font-size: 13px;
  508. color: #909399;
  509. max-width: 520px;
  510. }
  511. .clear-actions {
  512. display: flex;
  513. gap: 10px;
  514. align-items: center;
  515. flex-wrap: wrap;
  516. }
  517. .clear-result {
  518. margin-top: 14px;
  519. padding-top: 14px;
  520. border-top: 1px dashed #fde2e2;
  521. .result-title {
  522. font-size: 13px;
  523. color: #606266;
  524. margin-bottom: 8px;
  525. font-weight: 500;
  526. }
  527. .result-table {
  528. display: flex;
  529. flex-wrap: wrap;
  530. gap: 6px;
  531. }
  532. }
  533. }
  534. .import-grid {
  535. display: grid;
  536. grid-template-columns: repeat(2, 1fr);
  537. gap: 16px;
  538. }
  539. .ledger-card {
  540. height: 100%;
  541. :deep(.el-card__body) {
  542. padding: 16px;
  543. }
  544. .card-header {
  545. display: flex;
  546. align-items: center;
  547. gap: 8px;
  548. margin-bottom: 8px;
  549. }
  550. .card-icon {
  551. font-size: 20px;
  552. color: #409eff;
  553. flex-shrink: 0;
  554. }
  555. .card-title {
  556. font-size: 15px;
  557. font-weight: 600;
  558. color: #303133;
  559. line-height: 1.3;
  560. }
  561. .card-desc {
  562. font-size: 12px;
  563. color: #909399;
  564. margin-bottom: 12px;
  565. min-height: 32px;
  566. }
  567. .card-actions {
  568. display: flex;
  569. gap: 8px;
  570. align-items: center;
  571. flex-wrap: wrap;
  572. }
  573. .last-result {
  574. margin-top: 10px;
  575. font-size: 12px;
  576. display: flex;
  577. align-items: center;
  578. gap: 4px;
  579. padding: 4px 8px;
  580. border-radius: 4px;
  581. &.success {
  582. background: #f0f9eb;
  583. color: #67c23a;
  584. }
  585. &.error {
  586. background: #fef0f0;
  587. color: #f56c6c;
  588. }
  589. }
  590. }
  591. @media (max-width: 768px) {
  592. .import-grid {
  593. grid-template-columns: 1fr;
  594. }
  595. }
  596. </style>