StatusMaintenanceView.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <div class="page-fill status-maintenance-view">
  3. <PagePanel title="补助标准管理">
  4. <template #extra>
  5. <el-button
  6. v-if="canDistrictAdjust"
  7. type="warning"
  8. size="small"
  9. @click="openDistrictAdjust"
  10. >
  11. 全区调标
  12. </el-button>
  13. <el-button
  14. v-if="canMutateStandard"
  15. type="primary"
  16. size="small"
  17. :disabled="!canOpenCreate"
  18. :title="createBlockedTip || undefined"
  19. @click="openCreateDialog"
  20. >
  21. 新增标准
  22. </el-button>
  23. </template>
  24. <el-alert
  25. type="info"
  26. :closable="false"
  27. show-icon
  28. class="page-alert"
  29. :title="standardAlertTitle"
  30. />
  31. <el-alert
  32. v-if="pendingTip"
  33. type="warning"
  34. :closable="false"
  35. show-icon
  36. class="page-alert"
  37. :title="pendingTip"
  38. />
  39. <div class="page-fill__table">
  40. <el-table v-loading="standardLoading" :data="pagedStandards" border stripe size="small" height="100%">
  41. <el-table-column prop="year" label="年度" width="80" align="center" />
  42. <el-table-column label="月补助(元)" width="110" align="right">
  43. <template #default="{ row }">{{ formatAmount(row.monthly) }}</template>
  44. </el-table-column>
  45. <el-table-column label="丧葬补助(元)" width="120" align="right">
  46. <template #default="{ row }">{{ formatAmount(row.funeral) }}</template>
  47. </el-table-column>
  48. <el-table-column prop="date" label="生效日期" width="110" align="center" />
  49. <el-table-column prop="reason" label="调整原因" min-width="160" show-overflow-tooltip />
  50. <el-table-column label="当前" width="72" align="center">
  51. <template #default="{ row }">
  52. <el-tag v-if="isCurrentStandard(row as SubsidyStandard)" size="small" type="success">
  53. 有效
  54. </el-tag>
  55. <span v-else class="muted">历史</span>
  56. </template>
  57. </el-table-column>
  58. <el-table-column label="操作" width="200" align="center" fixed="right">
  59. <template #default="{ row }">
  60. <template v-if="isCurrentStandard(row as SubsidyStandard)">
  61. <el-button
  62. v-if="canMutateStandard"
  63. link
  64. type="primary"
  65. size="small"
  66. @click="openEditDialog(row as SubsidyStandard)"
  67. >
  68. 调整标准
  69. </el-button>
  70. <span v-else class="muted">现行标准</span>
  71. </template>
  72. <el-button
  73. v-else
  74. link
  75. type="primary"
  76. size="small"
  77. @click="openViewDialog(row as SubsidyStandard)"
  78. >
  79. 查看
  80. </el-button>
  81. <el-button
  82. v-if="isAdmin"
  83. link
  84. type="danger"
  85. size="small"
  86. @click="onDeleteStandard(row as SubsidyStandard)"
  87. >
  88. 删除
  89. </el-button>
  90. </template>
  91. </el-table-column>
  92. </el-table>
  93. </div>
  94. <TablePagination
  95. v-model:page="standardPage"
  96. v-model:page-size="standardPageSize"
  97. :total="standardTotal"
  98. />
  99. </PagePanel>
  100. <el-dialog v-model="viewDialogVisible" title="查看补助标准" width="480px" destroy-on-close>
  101. <el-descriptions v-if="currentStandard" :column="1" border size="small">
  102. <el-descriptions-item label="适用范围">全区统一</el-descriptions-item>
  103. <el-descriptions-item label="年度">{{ currentStandard.year }}</el-descriptions-item>
  104. <el-descriptions-item label="月补助">
  105. {{ formatAmount(currentStandard.monthly) }} 元
  106. </el-descriptions-item>
  107. <el-descriptions-item label="丧葬补助">
  108. {{ formatAmount(currentStandard.funeral) }} 元
  109. </el-descriptions-item>
  110. <el-descriptions-item label="生效日期">{{ currentStandard.date }}</el-descriptions-item>
  111. <el-descriptions-item label="调整原因">{{ currentStandard.reason }}</el-descriptions-item>
  112. </el-descriptions>
  113. <template #footer>
  114. <el-button type="primary" @click="viewDialogVisible = false">关闭</el-button>
  115. </template>
  116. </el-dialog>
  117. <el-dialog
  118. v-model="formDialogVisible"
  119. :title="formMode === 'create' ? '新增补助标准' : '调整补助标准'"
  120. width="520px"
  121. destroy-on-close
  122. @closed="onFormDialogClosed"
  123. >
  124. <el-form ref="formRef" :model="form" :rules="formRules" label-width="110px" size="small">
  125. <el-form-item label="年度" prop="year">
  126. <el-date-picker
  127. v-model="form.year"
  128. type="year"
  129. value-format="YYYY"
  130. placeholder="请选择年度"
  131. :disabled="formMode === 'edit' || formMode === 'create'"
  132. :disabled-date="disableNonCurrentYear"
  133. class="is-editable"
  134. style="width: 100%"
  135. />
  136. <p v-if="formMode === 'create'" class="status-maintenance-view__field-tip">
  137. 仅允许新增本年度({{ currentYear }})全区标准;本年度已有标准时请使用「调整标准」。
  138. </p>
  139. </el-form-item>
  140. <el-form-item label="月补助(元)" prop="monthly">
  141. <el-input-number
  142. v-model="form.monthly"
  143. :min="0"
  144. :precision="0"
  145. :controls="false"
  146. class="is-editable"
  147. style="width: 100%"
  148. placeholder="请输入月补助"
  149. />
  150. </el-form-item>
  151. <el-form-item label="丧葬补助(元)" prop="funeral">
  152. <el-input-number
  153. v-model="form.funeral"
  154. :min="0"
  155. :precision="0"
  156. :controls="false"
  157. class="is-editable"
  158. style="width: 100%"
  159. placeholder="请输入丧葬补助"
  160. />
  161. </el-form-item>
  162. <el-form-item label="生效日期" prop="date">
  163. <el-date-picker
  164. v-model="form.date"
  165. type="date"
  166. value-format="YYYY-MM-DD"
  167. placeholder="请选择生效日期"
  168. class="is-editable"
  169. style="width: 100%"
  170. />
  171. </el-form-item>
  172. <el-form-item label="调整原因" prop="reason">
  173. <el-input
  174. v-model="form.reason"
  175. type="textarea"
  176. :rows="2"
  177. class="is-editable"
  178. placeholder="请填写调整原因"
  179. />
  180. </el-form-item>
  181. </el-form>
  182. <template #footer>
  183. <el-button @click="formDialogVisible = false">取消</el-button>
  184. <el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
  185. </template>
  186. </el-dialog>
  187. <el-dialog
  188. v-model="districtVisible"
  189. title="全区调标"
  190. width="520px"
  191. destroy-on-close
  192. @closed="onDistrictClosed"
  193. >
  194. <el-alert
  195. type="warning"
  196. :closable="false"
  197. show-icon
  198. class="page-alert"
  199. title="将按全区统一新标准落库,并按各镇生成调标补发批次。街镇登录时提示已调标。"
  200. />
  201. <el-form ref="districtFormRef" :model="districtForm" :rules="districtRules" label-width="120px" size="small">
  202. <el-form-item label="新月补助(元)" prop="monthly">
  203. <el-input-number
  204. v-model="districtForm.monthly"
  205. :min="0"
  206. :precision="0"
  207. :controls="false"
  208. class="is-editable"
  209. style="width: 100%"
  210. />
  211. </el-form-item>
  212. <el-form-item label="新丧葬补助(元)" prop="funeral">
  213. <el-input-number
  214. v-model="districtForm.funeral"
  215. :min="0"
  216. :precision="0"
  217. :controls="false"
  218. class="is-editable"
  219. style="width: 100%"
  220. />
  221. </el-form-item>
  222. <el-form-item label="生效月份" prop="effectiveMonth">
  223. <el-date-picker
  224. v-model="districtForm.effectiveMonth"
  225. type="month"
  226. value-format="YYYYMM"
  227. placeholder="选择生效月份"
  228. class="is-editable"
  229. style="width: 100%"
  230. />
  231. </el-form-item>
  232. <el-form-item label="调整原因" prop="reason">
  233. <el-input
  234. v-model="districtForm.reason"
  235. type="textarea"
  236. :rows="2"
  237. class="is-editable"
  238. placeholder="请填写调标原因"
  239. />
  240. </el-form-item>
  241. </el-form>
  242. <template #footer>
  243. <el-button @click="districtVisible = false">取消</el-button>
  244. <el-button type="warning" :loading="districtSubmitting" @click="submitDistrictAdjust">确认全区调标</el-button>
  245. </template>
  246. </el-dialog>
  247. </div>
  248. </template>
  249. <script setup lang="ts">
  250. import { computed, onMounted, reactive, ref, watch } from 'vue'
  251. import type { FormInstance, FormRules } from 'element-plus'
  252. import { ElMessage, ElMessageBox } from 'element-plus'
  253. import PagePanel from '@/components/common/PagePanel.vue'
  254. import TablePagination from '@/components/common/TablePagination.vue'
  255. import {
  256. createStandard,
  257. deleteStandard,
  258. fetchStandardPendingTip,
  259. fetchStandards,
  260. submitDistrictYearlyAdjust,
  261. updateStandard,
  262. } from '@/api/biz'
  263. import { useClientPagination } from '@/composables/useClientPagination'
  264. import { guardBusy, runSubmit } from '@/composables/useSubmitLock'
  265. import { useUserStore } from '@/stores/user'
  266. import { formatAmount } from '@/utils/mask'
  267. import { isPositiveAmount } from '@/utils/validators'
  268. import type { SubsidyStandard } from '@/types/biz'
  269. const userStore = useUserStore()
  270. const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
  271. const isHr = computed(() => userStore.roleInfo?.key === 'hr')
  272. const canMutateStandard = computed(() => isAdmin.value)
  273. const canDistrictAdjust = computed(() => isHr.value || isAdmin.value)
  274. const standardAlertTitle = computed(() => {
  275. if (isHr.value) {
  276. return '补助标准全区统一,不按街镇分别维护。调标仅由区人社办理;确认后更新全区标准并按各镇生成补发批次。'
  277. }
  278. if (!isAdmin.value) {
  279. return '补助标准全区统一,由区人社调标。本页只可查看,不发起调标或补发。'
  280. }
  281. return '补助标准全区统一。标准变更默认需分管领导审批后生效;全区调标由区人社办理。'
  282. })
  283. const standards = ref<SubsidyStandard[]>([])
  284. const standardLoading = ref(false)
  285. const submitting = ref(false)
  286. const pendingTip = ref('')
  287. const viewDialogVisible = ref(false)
  288. const formDialogVisible = ref(false)
  289. const districtVisible = ref(false)
  290. const districtSubmitting = ref(false)
  291. const districtFormRef = ref<FormInstance>()
  292. const districtForm = reactive({
  293. monthly: undefined as number | undefined,
  294. funeral: undefined as number | undefined,
  295. effectiveMonth: '',
  296. reason: '',
  297. })
  298. function positiveMoneyRule(label: string) {
  299. return {
  300. validator: (_r: unknown, v: unknown, cb: (e?: Error) => void) => {
  301. if (!isPositiveAmount(v)) cb(new Error(`${label}须为大于 0 的金额,不能为 0、负数或中文`))
  302. else cb()
  303. },
  304. trigger: 'blur' as const,
  305. }
  306. }
  307. const districtRules: FormRules = {
  308. monthly: [positiveMoneyRule('新月补助')],
  309. funeral: [positiveMoneyRule('新丧葬补助')],
  310. effectiveMonth: [{ required: true, message: '请选择生效月份', trigger: 'change' }],
  311. reason: [{ required: true, message: '请填写调标原因', trigger: 'blur' }],
  312. }
  313. const formMode = ref<'create' | 'edit'>('create')
  314. const currentStandard = ref<SubsidyStandard | null>(null)
  315. const editingId = ref<number | null>(null)
  316. const formRef = ref<FormInstance>()
  317. /** 业务本年度:优先系统发放月,否则取自然年 */
  318. const currentYear = computed(() => {
  319. const m = String(userStore.currentMonth || '')
  320. if (/^\d{4}/.test(m)) return Number(m.slice(0, 4))
  321. return new Date().getFullYear()
  322. })
  323. function isCurrentStandard(row: SubsidyStandard) {
  324. return row.current === true
  325. }
  326. function hasCurrentYearStandard() {
  327. return standards.value.some((s) => Number(s.year) === currentYear.value)
  328. }
  329. const createBlockedTip = computed(() => {
  330. if (hasCurrentYearStandard()) {
  331. return `${currentYear.value} 年度已有全区标准,禁止新增;请使用「调整标准」`
  332. }
  333. return ''
  334. })
  335. const canOpenCreate = computed(() => canMutateStandard.value && !hasCurrentYearStandard())
  336. function disableNonCurrentYear(date: Date) {
  337. return date.getFullYear() !== currentYear.value
  338. }
  339. const form = reactive({
  340. town: '',
  341. year: '' as string,
  342. monthly: undefined as number | undefined,
  343. funeral: undefined as number | undefined,
  344. date: '' as string,
  345. reason: '',
  346. })
  347. const formRules: FormRules = {
  348. year: [{ required: true, message: '请选择年度', trigger: 'change' }],
  349. monthly: [positiveMoneyRule('月补助')],
  350. funeral: [positiveMoneyRule('丧葬补助')],
  351. date: [{ required: true, message: '请选择生效日期', trigger: 'change' }],
  352. reason: [{ required: true, message: '请填写调整原因', trigger: 'blur' }],
  353. }
  354. const {
  355. page: standardPage,
  356. pageSize: standardPageSize,
  357. total: standardTotal,
  358. pagedList: pagedStandards,
  359. resetPage: resetStandardPage,
  360. } = useClientPagination(standards)
  361. function resetForm() {
  362. form.town = '全区'
  363. form.year = String(currentYear.value)
  364. form.monthly = undefined
  365. form.funeral = undefined
  366. form.date = ''
  367. form.reason = ''
  368. editingId.value = null
  369. }
  370. function openViewDialog(row: SubsidyStandard) {
  371. currentStandard.value = row
  372. viewDialogVisible.value = true
  373. }
  374. function openCreateDialog() {
  375. if (hasCurrentYearStandard()) {
  376. ElMessage.warning(`${currentYear.value} 年度已有全区补助标准,请使用「调整标准」`)
  377. return
  378. }
  379. formMode.value = 'create'
  380. resetForm()
  381. form.year = String(currentYear.value)
  382. formDialogVisible.value = true
  383. }
  384. function openEditDialog(row: SubsidyStandard) {
  385. formMode.value = 'edit'
  386. editingId.value = row.id
  387. form.town = row.town
  388. form.year = String(row.year)
  389. form.monthly = row.monthly
  390. form.funeral = row.funeral
  391. form.date = row.date
  392. form.reason = row.reason
  393. formDialogVisible.value = true
  394. }
  395. function onFormDialogClosed() {
  396. formRef.value?.resetFields()
  397. resetForm()
  398. }
  399. async function submitForm() {
  400. const valid = await formRef.value?.validate().catch(() => false)
  401. if (!valid) return
  402. const year = Number(form.year)
  403. const monthly = form.monthly ?? 0
  404. const funeral = form.funeral ?? 0
  405. if (formMode.value === 'create') {
  406. if (year !== currentYear.value) {
  407. ElMessage.warning(`仅允许新增本年度(${currentYear.value})标准,不可新增历史或未来年度`)
  408. form.year = String(currentYear.value)
  409. return
  410. }
  411. if (hasCurrentYearStandard()) {
  412. ElMessage.warning(`${currentYear.value} 年度已有全区补助标准,请使用「调整标准」`)
  413. return
  414. }
  415. }
  416. if (guardBusy(submitting)) return
  417. submitting.value = true
  418. try {
  419. if (formMode.value === 'create') {
  420. const res = await createStandard({
  421. town: '全区',
  422. year: currentYear.value,
  423. monthly,
  424. funeral,
  425. date: form.date,
  426. reason: form.reason.trim(),
  427. })
  428. if (!res.ok) {
  429. ElMessage.warning(res.message || '新增失败')
  430. return
  431. }
  432. const tip = 'pendingTip' in res ? res.pendingTip : undefined
  433. if (tip) pendingTip.value = tip
  434. ElMessage.success(tip || '新增标准成功')
  435. } else if (editingId.value != null) {
  436. const editing = standards.value.find((s) => s.id === editingId.value)
  437. const res = await updateStandard({
  438. id: editingId.value,
  439. rawId: editing?.rawId,
  440. town: '全区',
  441. monthly,
  442. funeral,
  443. date: form.date,
  444. reason: form.reason.trim(),
  445. })
  446. if (!res.ok) {
  447. ElMessage.warning(res.message || '调整失败')
  448. return
  449. }
  450. const tip = 'pendingTip' in res ? res.pendingTip : undefined
  451. if (tip) pendingTip.value = tip
  452. ElMessage.success(tip || '调整标准成功')
  453. }
  454. formDialogVisible.value = false
  455. await loadStandards()
  456. } finally {
  457. submitting.value = false
  458. }
  459. }
  460. async function onDeleteStandard(row: SubsidyStandard) {
  461. const tip = isCurrentStandard(row)
  462. ? `月补 ${row.monthly} 元是当前全区有效标准,删除后将恢复上一档。确认删除?`
  463. : `确认删除 ${row.year} 年全区标准(月补 ${row.monthly})?`
  464. try {
  465. await ElMessageBox.confirm(tip, '删除补助标准', {
  466. type: 'warning',
  467. confirmButtonText: '确认删除',
  468. cancelButtonText: '取消',
  469. })
  470. } catch {
  471. return
  472. }
  473. await runSubmit(submitting, async () => {
  474. const res = await deleteStandard({ id: row.id, rawId: row.rawId })
  475. if (!res.ok) {
  476. ElMessage.warning(res.message || '删除失败')
  477. return
  478. }
  479. ElMessage.success(res.message || '已删除')
  480. await loadStandards()
  481. })
  482. }
  483. function openDistrictAdjust() {
  484. districtForm.monthly = undefined
  485. districtForm.funeral = undefined
  486. const m = String(userStore.currentMonth || '').replace('-', '')
  487. districtForm.effectiveMonth = /^\d{6}$/.test(m) ? m : ''
  488. districtForm.reason = ''
  489. districtVisible.value = true
  490. }
  491. function onDistrictClosed() {
  492. districtFormRef.value?.resetFields()
  493. }
  494. async function submitDistrictAdjust() {
  495. const valid = await districtFormRef.value?.validate().catch(() => false)
  496. if (!valid) return
  497. if (guardBusy(districtSubmitting)) return
  498. districtSubmitting.value = true
  499. try {
  500. await ElMessageBox.confirm(
  501. '确认按新标准完成全区统一调标?将更新全区一份标准,并按各镇生成补发批次。',
  502. '确认全区调标',
  503. { type: 'warning', confirmButtonText: '确认调标', cancelButtonText: '取消' },
  504. )
  505. const res = await submitDistrictYearlyAdjust({
  506. monthly: districtForm.monthly ?? 0,
  507. funeral: districtForm.funeral ?? 0,
  508. effectiveMonth: districtForm.effectiveMonth,
  509. reason: districtForm.reason.trim(),
  510. })
  511. const tip = String((res as { tip?: string })?.tip || '全区调标已完成,已生成各镇补发批次')
  512. ElMessage.success(tip)
  513. districtVisible.value = false
  514. await loadStandards()
  515. } catch (e) {
  516. if (e === 'cancel' || e === 'close') return
  517. ElMessage.error(e instanceof Error ? e.message : '全区调标失败')
  518. } finally {
  519. districtSubmitting.value = false
  520. }
  521. }
  522. async function loadStandards() {
  523. standardLoading.value = true
  524. try {
  525. standards.value = await fetchStandards()
  526. pendingTip.value = await fetchStandardPendingTip()
  527. resetStandardPage()
  528. } finally {
  529. standardLoading.value = false
  530. }
  531. }
  532. onMounted(loadStandards)
  533. watch(
  534. () => userStore.currentRole,
  535. () => {
  536. loadStandards()
  537. },
  538. )
  539. </script>
  540. <style scoped lang="less">
  541. .status-maintenance-view {
  542. :deep(.el-input-number .el-input__inner) {
  543. text-align: left;
  544. }
  545. &__filter {
  546. margin-bottom: 10px;
  547. padding: 8px 12px 0;
  548. }
  549. &__field-tip {
  550. margin: 4px 0 0;
  551. font-size: 12px;
  552. color: @text-secondary;
  553. line-height: 1.4;
  554. }
  555. }
  556. .muted {
  557. color: @text-secondary;
  558. font-size: 12px;
  559. }
  560. </style>