StatusMaintenanceView.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. <template>
  2. <div class="status-maintenance-view">
  3. <PagePanel title="补助标准管理">
  4. <template #extra>
  5. <el-button
  6. type="primary"
  7. size="small"
  8. :disabled="!canOpenCreate"
  9. :title="createBlockedTip || undefined"
  10. @click="openCreateDialog"
  11. >
  12. 新增标准
  13. </el-button>
  14. </template>
  15. <!-- 系统管理员:按街镇筛选全区标准 -->
  16. <div v-if="isAdmin" class="query-panel status-maintenance-view__filter">
  17. <el-form :inline="true" size="small">
  18. <el-form-item label="街镇">
  19. <el-select
  20. v-model="filterTown"
  21. placeholder="全部街镇"
  22. clearable
  23. style="width: 160px"
  24. @change="loadStandards"
  25. >
  26. <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
  27. </el-select>
  28. </el-form-item>
  29. <el-form-item>
  30. <el-button type="primary" @click="loadStandards">查询</el-button>
  31. <el-button @click="resetTownFilter">重置</el-button>
  32. </el-form-item>
  33. </el-form>
  34. </div>
  35. <el-table v-loading="standardLoading" :data="pagedStandards" border stripe size="small">
  36. <el-table-column prop="town" label="街镇" width="100" />
  37. <el-table-column prop="year" label="年度" width="80" align="center" />
  38. <el-table-column label="月补助(元)" width="110" align="right">
  39. <template #default="{ row }">{{ formatAmount(row.monthly) }}</template>
  40. </el-table-column>
  41. <el-table-column label="丧葬补助(元)" width="120" align="right">
  42. <template #default="{ row }">{{ formatAmount(row.funeral) }}</template>
  43. </el-table-column>
  44. <el-table-column prop="date" label="生效日期" width="110" align="center" />
  45. <el-table-column prop="reason" label="调整原因" min-width="160" show-overflow-tooltip />
  46. <el-table-column label="当前" width="72" align="center">
  47. <template #default="{ row }">
  48. <el-tag v-if="isCurrentStandard(row as SubsidyStandard)" size="small" type="success">
  49. 有效
  50. </el-tag>
  51. <span v-else class="muted">历史</span>
  52. </template>
  53. </el-table-column>
  54. <el-table-column label="操作" width="200" align="center" fixed="right">
  55. <template #default="{ row }">
  56. <template v-if="isCurrentStandard(row as SubsidyStandard)">
  57. <el-button
  58. link
  59. type="primary"
  60. size="small"
  61. @click="openEditDialog(row as SubsidyStandard)"
  62. >
  63. 调整标准
  64. </el-button>
  65. </template>
  66. <el-button
  67. v-else
  68. link
  69. type="primary"
  70. size="small"
  71. @click="openViewDialog(row as SubsidyStandard)"
  72. >
  73. 查看
  74. </el-button>
  75. <el-button
  76. v-if="isAdmin"
  77. link
  78. type="danger"
  79. size="small"
  80. @click="onDeleteStandard(row as SubsidyStandard)"
  81. >
  82. 删除
  83. </el-button>
  84. </template>
  85. </el-table-column>
  86. </el-table>
  87. <TablePagination
  88. v-model:page="standardPage"
  89. v-model:page-size="standardPageSize"
  90. :total="standardTotal"
  91. />
  92. </PagePanel>
  93. <el-dialog v-model="viewDialogVisible" title="查看补助标准" width="480px" destroy-on-close>
  94. <el-descriptions v-if="currentStandard" :column="1" border size="small">
  95. <el-descriptions-item label="街镇">{{ currentStandard.town }}</el-descriptions-item>
  96. <el-descriptions-item label="年度">{{ currentStandard.year }}</el-descriptions-item>
  97. <el-descriptions-item label="月补助">
  98. {{ formatAmount(currentStandard.monthly) }} 元
  99. </el-descriptions-item>
  100. <el-descriptions-item label="丧葬补助">
  101. {{ formatAmount(currentStandard.funeral) }} 元
  102. </el-descriptions-item>
  103. <el-descriptions-item label="生效日期">{{ currentStandard.date }}</el-descriptions-item>
  104. <el-descriptions-item label="调整原因">{{ currentStandard.reason }}</el-descriptions-item>
  105. </el-descriptions>
  106. <template #footer>
  107. <el-button type="primary" @click="viewDialogVisible = false">关闭</el-button>
  108. </template>
  109. </el-dialog>
  110. <el-dialog
  111. v-model="formDialogVisible"
  112. :title="formMode === 'create' ? '新增补助标准' : '调整补助标准'"
  113. width="520px"
  114. destroy-on-close
  115. @closed="onFormDialogClosed"
  116. >
  117. <el-form ref="formRef" :model="form" :rules="formRules" label-width="110px" size="small">
  118. <el-form-item label="街镇" prop="town">
  119. <el-select
  120. v-if="isAdmin && formMode === 'create'"
  121. v-model="form.town"
  122. placeholder="请选择街镇"
  123. class="is-editable"
  124. style="width: 100%"
  125. @change="onCreateTownChange"
  126. >
  127. <el-option v-for="t in townOptions" :key="t" :label="t" :value="t" />
  128. </el-select>
  129. <el-input v-else :model-value="form.town" disabled />
  130. </el-form-item>
  131. <el-form-item label="年度" prop="year">
  132. <el-date-picker
  133. v-model="form.year"
  134. type="year"
  135. value-format="YYYY"
  136. placeholder="请选择年度"
  137. :disabled="formMode === 'edit' || formMode === 'create'"
  138. :disabled-date="disableNonCurrentYear"
  139. class="is-editable"
  140. style="width: 100%"
  141. />
  142. <p v-if="formMode === 'create'" class="status-maintenance-view__field-tip">
  143. 仅允许新增本年度({{ currentYear }})标准;历史年度与未来年度不可新增。本街镇本年度已有标准时请使用「调整标准」。
  144. </p>
  145. </el-form-item>
  146. <el-form-item label="月补助(元)" prop="monthly">
  147. <el-input-number
  148. v-model="form.monthly"
  149. :min="0"
  150. :precision="0"
  151. :controls="false"
  152. class="is-editable"
  153. style="width: 100%"
  154. placeholder="请输入月补助"
  155. />
  156. </el-form-item>
  157. <el-form-item label="丧葬补助(元)" prop="funeral">
  158. <el-input-number
  159. v-model="form.funeral"
  160. :min="0"
  161. :precision="0"
  162. :controls="false"
  163. class="is-editable"
  164. style="width: 100%"
  165. placeholder="请输入丧葬补助"
  166. />
  167. </el-form-item>
  168. <el-form-item label="生效日期" prop="date">
  169. <el-date-picker
  170. v-model="form.date"
  171. type="date"
  172. value-format="YYYY-MM-DD"
  173. placeholder="请选择生效日期"
  174. class="is-editable"
  175. style="width: 100%"
  176. />
  177. </el-form-item>
  178. <el-form-item label="调整原因" prop="reason">
  179. <el-input
  180. v-model="form.reason"
  181. type="textarea"
  182. :rows="2"
  183. class="is-editable"
  184. placeholder="请填写调整原因"
  185. />
  186. </el-form-item>
  187. </el-form>
  188. <template #footer>
  189. <el-button @click="formDialogVisible = false">取消</el-button>
  190. <el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
  191. </template>
  192. </el-dialog>
  193. </div>
  194. </template>
  195. <script setup lang="ts">
  196. import { computed, onMounted, reactive, ref, watch } from 'vue'
  197. import type { FormInstance, FormRules } from 'element-plus'
  198. import { ElMessage, ElMessageBox } from 'element-plus'
  199. import PagePanel from '@/components/common/PagePanel.vue'
  200. import TablePagination from '@/components/common/TablePagination.vue'
  201. import { createStandard, deleteStandard, fetchStandards, updateStandard } from '@/api/biz'
  202. import { useClientPagination } from '@/composables/useClientPagination'
  203. import { useDynamicRegionOptions } from '@/composables/useTownVillageOptions'
  204. import { useUserStore } from '@/stores/user'
  205. import { formatAmount } from '@/utils/mask'
  206. import type { SubsidyStandard } from '@/types/biz'
  207. const userStore = useUserStore()
  208. const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
  209. const { townOptions } = useDynamicRegionOptions()
  210. const filterTown = ref('')
  211. const standards = ref<SubsidyStandard[]>([])
  212. const standardLoading = ref(false)
  213. const submitting = ref(false)
  214. const viewDialogVisible = ref(false)
  215. const formDialogVisible = ref(false)
  216. const formMode = ref<'create' | 'edit'>('create')
  217. const currentStandard = ref<SubsidyStandard | null>(null)
  218. const editingId = ref<number | null>(null)
  219. const formRef = ref<FormInstance>()
  220. const currentTown = computed(() => userStore.roleInfo?.town || '')
  221. /** 业务本年度:优先系统发放月,否则取自然年 */
  222. const currentYear = computed(() => {
  223. const m = String(userStore.currentMonth || '')
  224. if (/^\d{4}/.test(m)) return Number(m.slice(0, 4))
  225. return new Date().getFullYear()
  226. })
  227. /**
  228. * 是否当前有效标准:只认后端 current 标志。
  229. * 业务约定:每个街镇同一时间只应有一条有效标准(不按「日期最新」猜)。
  230. */
  231. function isCurrentStandard(row: SubsidyStandard) {
  232. return row.current === true
  233. }
  234. /** 某街镇是否已有本年度标准(有则禁止再新增) */
  235. function townHasCurrentYearStandard(town: string) {
  236. if (!town) return false
  237. const y = currentYear.value
  238. return standards.value.some((s) => s.town === town && Number(s.year) === y)
  239. }
  240. const createBlockedTip = computed(() => {
  241. if (isAdmin.value && !filterTown.value && !form.town) {
  242. return '请先选择街镇后再新增本年度标准'
  243. }
  244. const town = isAdmin.value ? filterTown.value || form.town : currentTown.value
  245. if (town && townHasCurrentYearStandard(town)) {
  246. return `「${town}」${currentYear.value} 年度已有标准,禁止新增;请使用「调整标准」`
  247. }
  248. return ''
  249. })
  250. /** 列表页「新增标准」按钮:管理员需先选镇,且本年度尚无标准 */
  251. const canOpenCreate = computed(() => {
  252. if (isAdmin.value) {
  253. if (!filterTown.value) return false
  254. return !townHasCurrentYearStandard(filterTown.value)
  255. }
  256. return Boolean(currentTown.value) && !townHasCurrentYearStandard(currentTown.value)
  257. })
  258. function disableNonCurrentYear(date: Date) {
  259. return date.getFullYear() !== currentYear.value
  260. }
  261. const form = reactive({
  262. town: '',
  263. year: '' as string,
  264. monthly: undefined as number | undefined,
  265. funeral: undefined as number | undefined,
  266. date: '' as string,
  267. reason: '',
  268. })
  269. const formRules: FormRules = {
  270. town: [{ required: true, message: '请选择街镇', trigger: 'change' }],
  271. year: [{ required: true, message: '请选择年度', trigger: 'change' }],
  272. monthly: [{ required: true, message: '请输入月补助', trigger: 'blur' }],
  273. funeral: [{ required: true, message: '请输入丧葬补助', trigger: 'blur' }],
  274. date: [{ required: true, message: '请选择生效日期', trigger: 'change' }],
  275. reason: [{ required: true, message: '请填写调整原因', trigger: 'blur' }],
  276. }
  277. const {
  278. page: standardPage,
  279. pageSize: standardPageSize,
  280. total: standardTotal,
  281. pagedList: pagedStandards,
  282. resetPage: resetStandardPage,
  283. } = useClientPagination(standards)
  284. function resetForm() {
  285. form.town = isAdmin.value ? filterTown.value || '' : currentTown.value
  286. form.year = String(currentYear.value)
  287. form.monthly = undefined
  288. form.funeral = undefined
  289. form.date = ''
  290. form.reason = ''
  291. editingId.value = null
  292. }
  293. function openViewDialog(row: SubsidyStandard) {
  294. currentStandard.value = row
  295. viewDialogVisible.value = true
  296. }
  297. function openCreateDialog() {
  298. const town = isAdmin.value ? filterTown.value : currentTown.value
  299. if (isAdmin.value && !town) {
  300. ElMessage.warning('请先在上方选择街镇后再新增')
  301. return
  302. }
  303. if (!town) {
  304. ElMessage.warning('当前账号未绑定街镇,无法新增标准')
  305. return
  306. }
  307. if (townHasCurrentYearStandard(town)) {
  308. ElMessage.warning(
  309. `「${town}」${currentYear.value} 年度已有补助标准,禁止重复新增;如需变更请点击「调整标准」`,
  310. )
  311. return
  312. }
  313. formMode.value = 'create'
  314. resetForm()
  315. form.year = String(currentYear.value)
  316. formDialogVisible.value = true
  317. }
  318. function onCreateTownChange(town: string) {
  319. if (formMode.value !== 'create') return
  320. form.year = String(currentYear.value)
  321. if (town && townHasCurrentYearStandard(town)) {
  322. ElMessage.warning(
  323. `「${town}」${currentYear.value} 年度已有补助标准,禁止新增;请关闭后使用「调整标准」`,
  324. )
  325. formDialogVisible.value = false
  326. }
  327. }
  328. function openEditDialog(row: SubsidyStandard) {
  329. formMode.value = 'edit'
  330. editingId.value = row.id
  331. form.town = row.town
  332. form.year = String(row.year)
  333. form.monthly = row.monthly
  334. form.funeral = row.funeral
  335. form.date = row.date
  336. form.reason = row.reason
  337. formDialogVisible.value = true
  338. }
  339. function onFormDialogClosed() {
  340. formRef.value?.resetFields()
  341. resetForm()
  342. }
  343. function resetTownFilter() {
  344. filterTown.value = ''
  345. loadStandards()
  346. }
  347. async function submitForm() {
  348. const valid = await formRef.value?.validate().catch(() => false)
  349. if (!valid) return
  350. const year = Number(form.year)
  351. const monthly = form.monthly ?? 0
  352. const funeral = form.funeral ?? 0
  353. if (formMode.value === 'create') {
  354. if (year !== currentYear.value) {
  355. ElMessage.warning(`仅允许新增本年度(${currentYear.value})标准,不可新增历史或未来年度`)
  356. form.year = String(currentYear.value)
  357. return
  358. }
  359. if (!form.town) {
  360. ElMessage.warning('请选择街镇')
  361. return
  362. }
  363. if (townHasCurrentYearStandard(form.town)) {
  364. ElMessage.warning(
  365. `「${form.town}」${currentYear.value} 年度已有补助标准,禁止重复新增;请使用「调整标准」`,
  366. )
  367. return
  368. }
  369. }
  370. submitting.value = true
  371. try {
  372. if (formMode.value === 'create') {
  373. const res = await createStandard({
  374. town: form.town,
  375. year: currentYear.value,
  376. monthly,
  377. funeral,
  378. date: form.date,
  379. reason: form.reason.trim(),
  380. })
  381. if (!res.ok) {
  382. ElMessage.warning(res.message || '新增失败')
  383. return
  384. }
  385. ElMessage.success('新增标准成功')
  386. } else if (editingId.value != null) {
  387. const editing = standards.value.find((s) => s.id === editingId.value)
  388. const res = await updateStandard({
  389. id: editingId.value,
  390. rawId: editing?.rawId,
  391. monthly,
  392. funeral,
  393. date: form.date,
  394. reason: form.reason.trim(),
  395. })
  396. if (!res.ok) {
  397. ElMessage.warning(res.message || '调整失败')
  398. return
  399. }
  400. ElMessage.success('调整标准成功')
  401. }
  402. formDialogVisible.value = false
  403. await loadStandards()
  404. } finally {
  405. submitting.value = false
  406. }
  407. }
  408. async function onDeleteStandard(row: SubsidyStandard) {
  409. const tip = isCurrentStandard(row)
  410. ? `「${row.town}」${row.monthly} 元是当前有效标准,删除后将自动恢复同镇上一档为有效。确认删除?`
  411. : `确认删除「${row.town}」${row.year} 年标准(月补 ${row.monthly})?删除后列表不再展示。`
  412. try {
  413. await ElMessageBox.confirm(tip, '删除补助标准', {
  414. type: 'warning',
  415. confirmButtonText: '确认删除',
  416. cancelButtonText: '取消',
  417. })
  418. } catch {
  419. return
  420. }
  421. const res = await deleteStandard({ id: row.id, rawId: row.rawId })
  422. if (!res.ok) {
  423. ElMessage.warning(res.message || '删除失败')
  424. return
  425. }
  426. ElMessage.success(res.message || '已删除')
  427. await loadStandards()
  428. }
  429. async function loadStandards() {
  430. standardLoading.value = true
  431. try {
  432. if (isAdmin.value) {
  433. const all = await fetchStandards()
  434. standards.value = filterTown.value
  435. ? all.filter((s) => s.town === filterTown.value)
  436. : all
  437. } else {
  438. standards.value = await fetchStandards(currentTown.value || undefined)
  439. }
  440. resetStandardPage()
  441. } finally {
  442. standardLoading.value = false
  443. }
  444. }
  445. onMounted(loadStandards)
  446. watch(
  447. () => userStore.currentRole,
  448. () => {
  449. filterTown.value = ''
  450. loadStandards()
  451. },
  452. )
  453. </script>
  454. <style scoped lang="less">
  455. .status-maintenance-view {
  456. :deep(.el-input-number .el-input__inner) {
  457. text-align: left;
  458. }
  459. &__filter {
  460. margin-bottom: 10px;
  461. padding: 8px 12px 0;
  462. }
  463. &__field-tip {
  464. margin: 4px 0 0;
  465. font-size: 12px;
  466. color: @text-secondary;
  467. line-height: 1.4;
  468. }
  469. }
  470. .muted {
  471. color: @text-secondary;
  472. font-size: 12px;
  473. }
  474. </style>