InsuranceMatchView.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. <template>
  2. <div class="page-fill insurance-match-view">
  3. <PagePanel :title="panelTitle">
  4. <template #extra>
  5. <el-button
  6. :loading="exporting"
  7. :disabled="!canExport"
  8. @click="onExport"
  9. >
  10. 导出结果
  11. </el-button>
  12. <el-button
  13. v-if="!isHistoryTab"
  14. type="primary"
  15. :loading="matching"
  16. :disabled="!selectedBatches.length || matching"
  17. @click="onBatchMatch"
  18. >
  19. 批量匹配
  20. </el-button>
  21. </template>
  22. <p class="page-tip">
  23. <template v-if="isAdmin && isHistoryTab">
  24. 已提交领导及之后的各镇居保结果,只读。请按街镇筛选。
  25. </template>
  26. <template v-else-if="isHistoryTab">
  27. 已提交领导及之后的本镇居保结果,只读,不可再次匹配。
  28. </template>
  29. <template v-else-if="isAdmin">
  30. 每月 1 号全区月批生成后自动居保。本页按街镇查看匹配结果;失败须由对应镇社区事务人工确认后才能提交该镇月度清单。
  31. </template>
  32. <template v-else>
  33. 核验全区当月批次的本镇人员。失败须在本页人工确认后,才能提交月度清单;已匹配完成的不会被全选勾中。
  34. </template>
  35. </p>
  36. <el-tabs v-model="activeTab" @tab-change="onTabChange">
  37. <el-tab-pane label="待办理" name="todo" />
  38. <el-tab-pane label="历史记录" name="history" />
  39. </el-tabs>
  40. <div class="query-panel insurance-match-view__filter">
  41. <el-form :inline="true" size="small">
  42. <el-form-item v-if="isAdmin" label="街镇">
  43. <el-select
  44. v-model="filterTown"
  45. placeholder="全部"
  46. clearable
  47. style="width: 140px"
  48. @change="onTownChange"
  49. >
  50. <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
  51. </el-select>
  52. </el-form-item>
  53. <el-form-item label="村居">
  54. <el-select v-model="filterVillage" placeholder="全部" clearable style="width: 140px">
  55. <el-option v-for="v in villageOptions" :key="v" :label="v" :value="v" />
  56. </el-select>
  57. </el-form-item>
  58. <el-form-item label="批次号">
  59. <el-input
  60. v-model="filterBatchNo"
  61. placeholder="如 202608"
  62. clearable
  63. style="width: 130px"
  64. @keyup.enter="applyFilter"
  65. />
  66. </el-form-item>
  67. <el-form-item label="流转状态">
  68. <el-select v-model="filterStage" placeholder="全部" clearable style="width: 150px">
  69. <el-option v-for="s in stageOptions" :key="s" :label="s" :value="s" />
  70. </el-select>
  71. </el-form-item>
  72. <el-form-item>
  73. <el-button type="primary" @click="applyFilter">查询</el-button>
  74. <el-button @click="resetFilter">重置</el-button>
  75. </el-form-item>
  76. </el-form>
  77. </div>
  78. <div class="page-fill__table">
  79. <el-table
  80. ref="tableRef"
  81. v-loading="batchLoading"
  82. :data="pagedBatches"
  83. border
  84. stripe
  85. size="small"
  86. height="100%"
  87. row-key="id"
  88. @selection-change="onSelectionChange"
  89. >
  90. <el-table-column
  91. v-if="!isHistoryTab"
  92. type="selection"
  93. width="42"
  94. align="center"
  95. :selectable="isBatchSelectable"
  96. />
  97. <el-table-column prop="batchNo" label="统一批次号" width="100" align="center" />
  98. <el-table-column prop="town" label="街镇" width="90" />
  99. <el-table-column label="范围" width="110">
  100. <template #default="{ row }">
  101. <el-button
  102. v-if="canViewMatchResult(row as VillageBatch)"
  103. link
  104. type="primary"
  105. @click="openVillageResult(row as VillageBatch)"
  106. >
  107. {{ isDistrictBatch(row as VillageBatch) && scopedTownName() ? '本镇' : row.village }}
  108. </el-button>
  109. <span v-else>{{ row.village }}</span>
  110. </template>
  111. </el-table-column>
  112. <el-table-column prop="peopleCount" label="人数" width="72" align="center" />
  113. <el-table-column label="预计金额(元)" width="120" align="right">
  114. <template #default="{ row }">{{ formatAmount(row.totalAmount) }}</template>
  115. </el-table-column>
  116. <el-table-column label="流转状态" width="130" align="center">
  117. <template #default="{ row }">
  118. <StatusTag :status="row.stage" />
  119. </template>
  120. </el-table-column>
  121. <el-table-column prop="submitter" label="提交人" width="110" />
  122. <el-table-column prop="submittedAt" label="提交时间" min-width="140" />
  123. <el-table-column label="操作" width="320" align="center" fixed="right">
  124. <template #default="{ row }">
  125. <el-button
  126. link
  127. type="primary"
  128. size="small"
  129. @click="openBatchDrawer(row as VillageBatch, 'detail')"
  130. >
  131. 批次详情
  132. </el-button>
  133. <el-button
  134. link
  135. type="primary"
  136. size="small"
  137. @click="openBatchDrawer(row as VillageBatch, 'flow')"
  138. >
  139. 流程记录
  140. </el-button>
  141. <el-button
  142. v-if="row.stage === '居保状态匹配'"
  143. link
  144. type="success"
  145. size="small"
  146. :loading="matching"
  147. :disabled="matching"
  148. @click="onMatchOne(row as VillageBatch)"
  149. >
  150. 匹配
  151. </el-button>
  152. <el-button
  153. v-else-if="canViewMatchResult(row as VillageBatch)"
  154. link
  155. type="success"
  156. size="small"
  157. @click="openVillageResult(row as VillageBatch)"
  158. >
  159. 匹配结果
  160. </el-button>
  161. <el-button
  162. v-if="canViewMatchResult(row as VillageBatch)"
  163. link
  164. type="primary"
  165. size="small"
  166. :loading="exportingBatchId === row.id"
  167. @click="onExportBatch(row as VillageBatch)"
  168. >
  169. 导出
  170. </el-button>
  171. </template>
  172. </el-table-column>
  173. </el-table>
  174. </div>
  175. <TablePagination
  176. v-model:page="batchPage"
  177. v-model:page-size="batchPageSize"
  178. :total="batchTotal"
  179. />
  180. </PagePanel>
  181. <!-- 可研 5.1.4.1:任务态 / 失败清单 / 执行日志 -->
  182. <PagePanel v-if="lastTaskSummary || taskLogs.length" title="居保匹配任务执行情况">
  183. <p class="insurance-match-view__tip">
  184. 最近一次匹配的人数汇总、待人工处理名单与执行日志。
  185. </p>
  186. <div v-if="lastTaskSummary" class="insurance-match-view__task-summary">
  187. <el-tag type="info" effect="plain">
  188. 批次号 {{ taskBatchNoLabel }}
  189. </el-tag>
  190. <el-tag type="success" effect="plain">成功 {{ lastTaskSummary.successCount ?? 0 }} 人</el-tag>
  191. <el-tag type="danger" effect="plain">失败 {{ lastTaskSummary.failCount ?? 0 }} 人</el-tag>
  192. <el-tag type="warning" effect="plain">待确认 {{ lastTaskSummary.manualCount ?? 0 }} 人</el-tag>
  193. <el-tag effect="plain">
  194. 批次下一节点 {{ displayBizText(String(lastTaskSummary.nextStatus || '')) }}
  195. </el-tag>
  196. </div>
  197. <el-table
  198. v-if="(lastTaskSummary?.failItems as Record<string, unknown>[] | undefined)?.length"
  199. :data="(lastTaskSummary?.failItems as Record<string, unknown>[]) || []"
  200. border
  201. stripe
  202. size="small"
  203. max-height="220"
  204. style="margin: 10px 0"
  205. >
  206. <el-table-column prop="name" label="姓名" width="100" />
  207. <el-table-column label="身份证" min-width="160">
  208. <template #default="{ row }">{{ maskIdCard(String(row.idNumber || '')) }}</template>
  209. </el-table-column>
  210. <el-table-column label="匹配结果" width="120">
  211. <template #default="{ row }">
  212. {{ labelOf('insuranceMatch', String(row.matchResult || '')) || '—' }}
  213. </template>
  214. </el-table-column>
  215. <el-table-column prop="message" label="失败/待确认原因" min-width="220" show-overflow-tooltip />
  216. </el-table>
  217. <el-table
  218. v-loading="taskLogLoading"
  219. :data="taskLogs"
  220. border
  221. stripe
  222. size="small"
  223. max-height="220"
  224. >
  225. <el-table-column label="办理动作" width="140">
  226. <template #default="{ row }">
  227. {{ processActionLabelOf(String(row.action || '')) }}
  228. </template>
  229. </el-table-column>
  230. <el-table-column prop="operator" label="操作人" width="120" />
  231. <el-table-column label="结果" width="90">
  232. <template #default="{ row }">
  233. {{ resultLabelOf(String(row.result || '')) }}
  234. </template>
  235. </el-table-column>
  236. <el-table-column prop="opinion" label="摘要" min-width="240" show-overflow-tooltip />
  237. <el-table-column label="时间" width="160">
  238. <template #default="{ row }">
  239. {{ formatTaskTime(row.operateTime) }}
  240. </template>
  241. </el-table-column>
  242. </el-table>
  243. </PagePanel>
  244. <BatchDetailDrawer
  245. v-model="batchDrawerVisible"
  246. :batch-id="batchDrawerId"
  247. :initial-tab="batchDrawerTab"
  248. />
  249. <!-- 各村人员居保匹配结果弹窗 -->
  250. <el-dialog
  251. v-model="resultDialogVisible"
  252. :title="resultDialogTitle"
  253. width="900px"
  254. destroy-on-close
  255. @closed="onResultDialogClosed"
  256. >
  257. <el-table v-loading="resultLoading" :data="pagedResults" border stripe size="small" max-height="460">
  258. <el-table-column prop="name" label="姓名" width="80" />
  259. <el-table-column label="身份证" min-width="160">
  260. <template #default="{ row }">{{ maskIdCard(row.idCard) }}</template>
  261. </el-table-column>
  262. <el-table-column prop="town" label="街镇" width="90" />
  263. <el-table-column prop="village" label="村居" width="90" />
  264. <el-table-column label="居保状态" width="110" align="center">
  265. <template #default="{ row }">
  266. <StatusTag :status="row.insuranceStatus" />
  267. </template>
  268. </el-table-column>
  269. <el-table-column label="人员状态" width="110" align="center">
  270. <template #default="{ row }">
  271. <StatusTag :status="row.status" />
  272. </template>
  273. </el-table-column>
  274. <el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
  275. <el-table-column label="操作" width="180" align="center" fixed="right">
  276. <template #default="{ row }">
  277. <el-button
  278. v-if="!isHistoryTab && row.insuranceStatus !== '匹配通过'"
  279. link
  280. type="primary"
  281. size="small"
  282. @click="openManualConfirm(row as Person)"
  283. >
  284. 人工确认
  285. </el-button>
  286. <el-button
  287. v-if="!isHistoryTab"
  288. link
  289. type="warning"
  290. size="small"
  291. :loading="rematchingId === String(row.id)"
  292. @click="onRematchOne(row as Person)"
  293. >
  294. 重新匹配
  295. </el-button>
  296. <span v-else-if="isHistoryTab && row.insuranceStatus === '匹配通过'" class="muted">已通过</span>
  297. </template>
  298. </el-table-column>
  299. </el-table>
  300. <TablePagination
  301. v-model:page="resultPage"
  302. v-model:page-size="resultPageSize"
  303. :total="resultTotal"
  304. />
  305. </el-dialog>
  306. <!-- 人工确认:继续发放 / 暂停发放 -->
  307. <el-dialog
  308. v-model="manualDialogVisible"
  309. title="居保匹配人工确认"
  310. width="480px"
  311. destroy-on-close
  312. @closed="onManualDialogClosed"
  313. >
  314. <template v-if="manualTarget">
  315. <p class="insurance-match-view__tip">
  316. 人员:{{ manualTarget.name }}({{ maskIdCard(manualTarget.idCard) }})
  317. </p>
  318. <p v-if="manualTarget.remark" class="insurance-match-view__match-msg">
  319. 最近匹配说明:{{ manualTarget.remark }}
  320. </p>
  321. <el-form label-width="80px" size="small">
  322. <el-form-item label="确认备注">
  323. <el-input
  324. v-model="manualRemark"
  325. type="textarea"
  326. :rows="3"
  327. maxlength="200"
  328. show-word-limit
  329. placeholder="请填写人工确认说明"
  330. />
  331. </el-form-item>
  332. </el-form>
  333. </template>
  334. <template #footer>
  335. <el-button @click="manualDialogVisible = false">取消</el-button>
  336. <el-button type="warning" :loading="manualSaving" @click="submitManualConfirm(false)">
  337. 暂停发放
  338. </el-button>
  339. <el-button type="success" :loading="manualSaving" @click="submitManualConfirm(true)">
  340. 继续发放
  341. </el-button>
  342. </template>
  343. </el-dialog>
  344. </div>
  345. </template>
  346. <script setup lang="ts">
  347. import { computed, onMounted, ref, watch } from 'vue'
  348. import { ElMessage } from 'element-plus'
  349. import type { TableInstance } from 'element-plus'
  350. import PagePanel from '@/components/common/PagePanel.vue'
  351. import StatusTag from '@/components/common/StatusTag.vue'
  352. import TablePagination from '@/components/common/TablePagination.vue'
  353. import {
  354. confirmInsuranceManual,
  355. exportInsuranceMatch,
  356. fetchInsuranceBatchesPage,
  357. fetchInsuranceResultsPage,
  358. fetchInsuranceTaskLogs,
  359. triggerInsuranceMatch,
  360. } from '@/api/biz'
  361. import { useServerPagination } from '@/composables/useServerPagination'
  362. import { useTownVillageOptions } from '@/composables/useTownVillageOptions'
  363. import { guardBusy, runSubmit } from '@/composables/useSubmitLock'
  364. import { useUserStore } from '@/stores/user'
  365. import BatchDetailDrawer from '@/components/biz/BatchDetailDrawer.vue'
  366. import { maskIdCard, formatAmount } from '@/utils/mask'
  367. import {
  368. displayBizText,
  369. labelOf,
  370. processActionLabelOf,
  371. resultLabelOf,
  372. } from '@/utils/statusLabels'
  373. import type { Person, VillageBatch } from '@/types/biz'
  374. import { resolveScopeIds } from '@/api/sjnmtybt/regionStore'
  375. const userStore = useUserStore()
  376. const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
  377. const {
  378. filterTown,
  379. filterVillage,
  380. townOptions,
  381. villageOptions,
  382. onTownChange,
  383. resetTownVillage,
  384. loadVillages,
  385. } = useTownVillageOptions()
  386. const activeTab = ref('todo')
  387. const isHistoryTab = computed(() => activeTab.value === 'history')
  388. const filterBatchNo = ref('')
  389. const filterStage = ref('')
  390. const panelTitle = computed(() =>
  391. isHistoryTab.value ? '居保匹配历史记录' : '全区月批居保匹配(本镇切片)',
  392. )
  393. const stageOptions = computed(() =>
  394. isHistoryTab.value
  395. ? ['分管领导审批', '待线下发放', '回盘信息确认', '已回盘确认', '已归档']
  396. : ['居保状态匹配', '比对结果已生成'],
  397. )
  398. const resultLoading = computed(() => resultPagerLoading.value)
  399. const matching = ref(false)
  400. const rematchingId = ref('')
  401. const exporting = ref(false)
  402. const exportingBatchId = ref('')
  403. const selectedBatches = ref<VillageBatch[]>([])
  404. const tableRef = ref<TableInstance>()
  405. const canExport = computed(() => {
  406. if (selectedBatches.value.length === 1) return true
  407. if (isAdmin.value) return !!filterTown.value
  408. return !!userStore.roleInfo?.town
  409. })
  410. const resultDialogVisible = ref(false)
  411. const currentBatch = ref<VillageBatch | null>(null)
  412. const batchDrawerVisible = ref(false)
  413. const batchDrawerId = ref('')
  414. const batchDrawerTab = ref('detail')
  415. const manualDialogVisible = ref(false)
  416. const manualTarget = ref<Person | null>(null)
  417. const manualRemark = ref('')
  418. const manualSaving = ref(false)
  419. const lastTaskSummary = ref<Record<string, unknown> | null>(null)
  420. const taskLogs = ref<Record<string, unknown>[]>([])
  421. const taskLogLoading = ref(false)
  422. /** 任务摘要展示批次号(禁止直接渲染内部 batchId) */
  423. const taskBatchNoLabel = computed(() => {
  424. const id = String(lastTaskSummary.value?.batchId || '')
  425. if (!id) return '—'
  426. const hit = batches.value.find((b) => b.id === id)
  427. return hit?.batchNo || id.replace(/^VB/, '')
  428. })
  429. function formatTaskTime(ts: unknown) {
  430. const n = Number(ts)
  431. if (!n) return '—'
  432. const d = new Date(n)
  433. if (Number.isNaN(d.getTime())) return '—'
  434. const p = (x: number) => String(x).padStart(2, '0')
  435. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
  436. }
  437. async function loadTaskLogs(batchId: string) {
  438. if (!batchId) {
  439. taskLogs.value = []
  440. return
  441. }
  442. taskLogLoading.value = true
  443. try {
  444. taskLogs.value = await fetchInsuranceTaskLogs(batchId)
  445. } catch {
  446. taskLogs.value = []
  447. } finally {
  448. taskLogLoading.value = false
  449. }
  450. }
  451. function openBatchDrawer(row: VillageBatch, tab: 'detail' | 'flow' = 'detail') {
  452. if (!row.id) return
  453. batchDrawerId.value = row.id
  454. batchDrawerTab.value = tab
  455. batchDrawerVisible.value = true
  456. }
  457. const resultDialogTitle = computed(() => {
  458. const b = currentBatch.value
  459. if (!b) return '人员居保匹配情况'
  460. const town = scopedTownName()
  461. if (town) return `${town}(批次 ${b.batchNo})本镇居保匹配`
  462. return `全区(批次 ${b.batchNo})居保匹配情况`
  463. })
  464. /** 非管理员必须用当前角色街镇;管理员用筛选街镇。全区月批本身没有街镇字段。 */
  465. function scopedTownName() {
  466. if (isAdmin.value) return filterTown.value || ''
  467. return userStore.roleInfo?.town || ''
  468. }
  469. function isDistrictBatch(b: VillageBatch) {
  470. return b.batchLevel === 'DISTRICT' || b.village === '全区' || (!b.town && !b.village)
  471. }
  472. const {
  473. page: batchPage,
  474. pageSize: batchPageSize,
  475. total: batchTotal,
  476. list: pagedBatches,
  477. loading: batchLoading,
  478. resetAndLoad: loadBatches,
  479. } = useServerPagination<VillageBatch>(async (pageNum, pageSize) => {
  480. selectedBatches.value = []
  481. const townName = isAdmin.value ? filterTown.value || '' : scopedTownName()
  482. const { townId } = await resolveScopeIds(townName || undefined, undefined)
  483. return fetchInsuranceBatchesPage({
  484. tab: isHistoryTab.value ? 'history' : 'todo',
  485. batchNo: filterBatchNo.value.trim() || undefined,
  486. stage: filterStage.value || undefined,
  487. townId: townId || undefined,
  488. pageNum,
  489. pageSize,
  490. })
  491. })
  492. const batches = pagedBatches
  493. const {
  494. page: resultPage,
  495. pageSize: resultPageSize,
  496. total: resultTotal,
  497. list: pagedResults,
  498. loading: resultPagerLoading,
  499. resetAndLoad: loadResultPage,
  500. } = useServerPagination<Person>(async (pageNum, pageSize) => {
  501. const row = currentBatch.value
  502. if (!row) return { list: [], total: 0 }
  503. return fetchInsuranceResultsPage({
  504. batchId: row.id,
  505. batchNo: row.batchNo,
  506. town: scopedTownName() || undefined,
  507. village: filterVillage.value || undefined,
  508. pageNum,
  509. pageSize,
  510. })
  511. })
  512. function applyFilter() {
  513. void loadBatches()
  514. }
  515. function resetFilter() {
  516. filterBatchNo.value = ''
  517. filterStage.value = ''
  518. if (isAdmin.value) {
  519. resetTownVillage()
  520. } else {
  521. filterVillage.value = ''
  522. }
  523. void loadBatches()
  524. }
  525. async function syncScopeTown() {
  526. const town = userStore.roleInfo?.town
  527. if (!isAdmin.value && town) {
  528. filterTown.value = town
  529. await loadVillages(town)
  530. }
  531. }
  532. /** 已匹配完成的村不可勾选,全选时也不会被选中 */
  533. function isBatchSelectable(row: VillageBatch) {
  534. return row.stage === '居保状态匹配'
  535. }
  536. function canViewMatchResult(row: VillageBatch) {
  537. return row.stage !== '居保状态匹配'
  538. }
  539. function onSelectionChange(rows: VillageBatch[]) {
  540. selectedBatches.value = rows
  541. }
  542. async function onBatchMatch() {
  543. const ids = selectedBatches.value
  544. .filter((b) => b.stage === '居保状态匹配')
  545. .map((b) => b.id)
  546. if (!ids.length) {
  547. ElMessage.warning('请先勾选待匹配的村居')
  548. return
  549. }
  550. if (guardBusy(matching)) return
  551. matching.value = true
  552. try {
  553. const res = (await triggerInsuranceMatch(ids)) as {
  554. ok?: boolean
  555. count?: number
  556. lastSummary?: Record<string, unknown> | null
  557. }
  558. if (res?.lastSummary) {
  559. lastTaskSummary.value = res.lastSummary
  560. await loadTaskLogs(String(res.lastSummary.batchId || ids[ids.length - 1]))
  561. }
  562. const fail = Number(res?.lastSummary?.failCount || 0)
  563. const countTip = typeof res?.count === 'number' ? `(${res.count} 个村)` : ''
  564. ElMessage.success(
  565. fail > 0
  566. ? `居保匹配完成${countTip},存在 ${fail} 人失败/待确认,请查看下方任务清单`
  567. : `已完成居保匹配${countTip},比对结果清单已生成`,
  568. )
  569. selectedBatches.value = []
  570. tableRef.value?.clearSelection()
  571. await loadBatches()
  572. } finally {
  573. matching.value = false
  574. }
  575. }
  576. async function onMatchOne(row: VillageBatch) {
  577. await runSubmit(matching, async () => {
  578. const res = (await triggerInsuranceMatch(row.id)) as {
  579. lastSummary?: Record<string, unknown> | null
  580. }
  581. if (res?.lastSummary) {
  582. lastTaskSummary.value = res.lastSummary
  583. }
  584. await loadTaskLogs(row.id)
  585. const fail = Number(res?.lastSummary?.failCount || 0)
  586. ElMessage.success(
  587. fail > 0
  588. ? `${row.village}匹配完成,有 ${fail} 人需处理`
  589. : `${row.village}比对结果清单已生成`,
  590. )
  591. await loadBatches()
  592. })
  593. }
  594. async function openVillageResult(row: VillageBatch) {
  595. currentBatch.value = row
  596. resultDialogVisible.value = true
  597. await Promise.all([loadVillageResults(row), loadTaskLogs(row.id)])
  598. }
  599. async function loadVillageResults(row: VillageBatch) {
  600. currentBatch.value = row
  601. await loadResultPage()
  602. }
  603. function onResultDialogClosed() {
  604. currentBatch.value = null
  605. }
  606. function openManualConfirm(row: Person) {
  607. manualTarget.value = row
  608. manualRemark.value = row.remark || ''
  609. manualDialogVisible.value = true
  610. }
  611. function onManualDialogClosed() {
  612. manualTarget.value = null
  613. manualRemark.value = ''
  614. }
  615. async function submitManualConfirm(pass: boolean) {
  616. if (!manualTarget.value) return
  617. if (guardBusy(manualSaving)) return
  618. manualSaving.value = true
  619. try {
  620. await confirmInsuranceManual(manualTarget.value.id, {
  621. batchId: currentBatch.value?.id,
  622. pass,
  623. remark: manualRemark.value.trim() || undefined,
  624. })
  625. ElMessage.success(
  626. pass
  627. ? '已确认继续发放。全部处理完后请到「月补贴清单」提交分管领导审核'
  628. : '已确认暂停发放',
  629. )
  630. manualDialogVisible.value = false
  631. if (currentBatch.value) {
  632. await loadVillageResults(currentBatch.value)
  633. }
  634. } finally {
  635. manualSaving.value = false
  636. }
  637. }
  638. async function onRematchOne(row: Person) {
  639. if (!currentBatch.value?.id) return
  640. if (guardBusy(matching)) return
  641. rematchingId.value = String(row.id)
  642. matching.value = true
  643. try {
  644. await triggerInsuranceMatch(currentBatch.value.id, String(row.id))
  645. ElMessage.success(`${row.name || '该人员'}已重新匹配`)
  646. await loadVillageResults(currentBatch.value)
  647. } finally {
  648. rematchingId.value = ''
  649. matching.value = false
  650. }
  651. }
  652. async function onExportBatch(row: VillageBatch) {
  653. if (!row.id) return
  654. if (guardBusy(exportingBatchId)) return
  655. exportingBatchId.value = row.id
  656. try {
  657. const res = await exportInsuranceMatch({
  658. batchId: row.id,
  659. town: scopedTownName() || undefined,
  660. })
  661. if (!res?.ok) {
  662. ElMessage.warning(res?.message || '导出失败')
  663. return
  664. }
  665. ElMessage.success(`已导出 ${res.fileName || '居保匹配结果'}`)
  666. } finally {
  667. exportingBatchId.value = ''
  668. }
  669. }
  670. async function onExport() {
  671. if (selectedBatches.value.length === 1) {
  672. await onExportBatch(selectedBatches.value[0])
  673. return
  674. }
  675. const town = isAdmin.value ? filterTown.value : userStore.roleInfo?.town
  676. if (!town) {
  677. ElMessage.warning(isAdmin.value ? '请先选择街镇,或勾选单个批次导出' : '未获取到当前街镇')
  678. return
  679. }
  680. if (guardBusy(exporting)) return
  681. exporting.value = true
  682. try {
  683. const res = await exportInsuranceMatch({ town })
  684. if (!res?.ok) {
  685. ElMessage.warning(res?.message || '导出失败')
  686. return
  687. }
  688. ElMessage.success(`已导出 ${res.fileName || '居保匹配结果'}`)
  689. } finally {
  690. exporting.value = false
  691. }
  692. }
  693. function onTabChange() {
  694. filterStage.value = ''
  695. tableRef.value?.clearSelection()
  696. loadBatches()
  697. }
  698. onMounted(async () => {
  699. await syncScopeTown()
  700. await loadBatches()
  701. })
  702. watch(
  703. () => userStore.currentRole,
  704. async () => {
  705. filterBatchNo.value = ''
  706. filterStage.value = ''
  707. if (isAdmin.value) resetTownVillage()
  708. else {
  709. filterVillage.value = ''
  710. await syncScopeTown()
  711. }
  712. await loadBatches()
  713. },
  714. )
  715. </script>
  716. <style scoped lang="less">
  717. .insurance-match-view__task-summary {
  718. display: flex;
  719. flex-wrap: wrap;
  720. gap: 8px;
  721. margin-bottom: 4px;
  722. }
  723. .insurance-match-view__tip {
  724. margin: 0 0 10px;
  725. font-size: 12px;
  726. color: @text-secondary;
  727. line-height: 1.5;
  728. }
  729. .insurance-match-view__filter {
  730. margin-bottom: 10px;
  731. padding: 8px 12px 0;
  732. }
  733. .muted {
  734. color: @text-secondary;
  735. font-size: 12px;
  736. }
  737. .insurance-match-view__match-msg {
  738. margin: 0 0 12px;
  739. padding: 8px 10px;
  740. font-size: 12px;
  741. line-height: 1.5;
  742. color: @text-regular;
  743. background: @table-header-bg;
  744. border-radius: @radius-sm;
  745. }
  746. </style>