evaluation-form-review.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <template>
  2. <div class="about main-background main-background-type0">
  3. <div class="nav-placeholder" />
  4. <section class="main-section large">
  5. <div class="content">
  6. <div class="title left-right">
  7. <a-button :icon="h(ArrowLeftOutlined)" @click="router.back()">返回</a-button>
  8. <h2>自查评估表审核</h2>
  9. <div class="w-20" />
  10. </div>
  11. <a-spin :spinning="loader.loading.value">
  12. <a-result
  13. v-if="!currentForm"
  14. status="warning"
  15. title="无法加载评估表"
  16. sub-title="请从管理员列表进入,并确认自查表 ID 与传承人用户 ID 正确。"
  17. />
  18. <div v-else>
  19. <a-alert
  20. type="info"
  21. show-icon
  22. class="mb-4"
  23. :message="progressHint"
  24. />
  25. <SelfAssessmentFormDisplay
  26. :current-form="(currentForm as SelfAssessmentDetail)"
  27. :check-item-list="(checkItemList as CheckItemInfo[])"
  28. :current-form-check-items="(currentFormCheckItems as SelfAssessmentCheckItemAnswer[])"
  29. :readonly="true"
  30. />
  31. <a-divider />
  32. <a-card title="审核提交" size="small">
  33. <a-alert
  34. v-if="!canSubmitReview"
  35. type="warning"
  36. show-icon
  37. class="mb-4"
  38. message="当前账号用户组无权在此环节审核"
  39. :description="`仅项目保护单位(9)、县(区)文旅部门(5)、设区市文旅部门/省非遗中心(10)、省文化和旅游厅(11) 可提交。`"
  40. />
  41. <a-form layout="vertical" class="max-w-xl">
  42. <a-form-item :label="`审核环节:${reviewLevelLabel}`">
  43. <a-select
  44. v-model:value="reviewOpinion"
  45. allow-clear
  46. placeholder="请选择审核意见"
  47. :options="opinionSelectOptions"
  48. />
  49. </a-form-item>
  50. <a-form-item label="评分">
  51. <a-input-number
  52. v-model:value="reviewPoints"
  53. :min="0"
  54. :max="100"
  55. class="w-full"
  56. placeholder="可选"
  57. />
  58. </a-form-item>
  59. <a-form-item label="提交后进度">
  60. <a-input :value="progressSubmitLabel" readonly />
  61. </a-form-item>
  62. <a-form-item>
  63. <a-space>
  64. <a-button
  65. type="primary"
  66. :loading="submitLoading"
  67. :disabled="!canSubmitReview"
  68. @click="submitReview"
  69. >
  70. 提交审核
  71. </a-button>
  72. <a-button
  73. danger
  74. :loading="rejectLoading"
  75. :disabled="!canSubmitReview"
  76. @click="submitReject"
  77. >
  78. 回退
  79. </a-button>
  80. </a-space>
  81. </a-form-item>
  82. </a-form>
  83. </a-card>
  84. </div>
  85. </a-spin>
  86. </div>
  87. </section>
  88. </div>
  89. </template>
  90. <script setup lang="ts">
  91. import { computed, h, onMounted, ref, watch } from 'vue';
  92. import { useRoute, useRouter } from 'vue-router';
  93. import { Input, message, Modal } from 'ant-design-vue';
  94. import { RequestApiError } from '@imengyu/imengyu-utils';
  95. import { ArrowLeftOutlined } from '@ant-design/icons-vue';
  96. import AssessmentContentApi, {
  97. type CheckItemInfo,
  98. type SelfAssessmentCheckItemAnswer,
  99. SelfAssessmentDetail,
  100. } from '@/api/collect/AssessmentContent';
  101. import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vue';
  102. import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
  103. import { useAuthStore } from '@/stores/auth';
  104. function formatErr(e: unknown): string {
  105. if (e instanceof RequestApiError)
  106. return e.errorMessage;
  107. if (e instanceof Error)
  108. return e.message;
  109. return String(e);
  110. }
  111. const router = useRouter();
  112. const route = useRoute();
  113. const authStore = useAuthStore();
  114. const currentUserGroups = computed(() => authStore.userInfo?.adminGroup || []);
  115. const queryFormId = computed(() => Number(route.query.id) || 0);
  116. const queryUserId = computed(() => Number(route.query.userId) || 0);
  117. /** 列表当前进度(与管理员列表一致) */
  118. const listProgress = computed(() => {
  119. const p = Number(route.query.progress);
  120. return Number.isFinite(p) ? p : NaN;
  121. });
  122. const currentForm = ref<SelfAssessmentDetail | null>(null);
  123. const currentFormCheckItems = ref([] as SelfAssessmentCheckItemAnswer[]);
  124. const checkItemList = ref([] as CheckItemInfo[]);
  125. const submitLoading = ref(false);
  126. const rejectLoading = ref(false);
  127. const reviewOpinion = ref<number | null>(null);
  128. const reviewPoints = ref<number | null>(null);
  129. /** 用户组 → 本环节提交后的 progress */
  130. const GROUP_TO_REVIEW_PROGRESS: Record<number, number> = {
  131. 9: 2, // 项目保护单位
  132. 5: 3, // 县(区)文旅部门
  133. 10: 4, // 设区市文旅部门、省非遗中心
  134. 11: 5, // 省文化和旅游厅
  135. };
  136. const PROGRESS_SUBMIT_LABELS: Record<number, string> = {
  137. 2: '2 — 项目保护单位审核完成',
  138. 3: '3 — 县(区)文旅部门审核完成',
  139. 4: '4 — 设区市文旅部门、省非遗中心审核完成',
  140. 5: '5 — 省文化和旅游厅审核完成',
  141. };
  142. const opinionSelectOptions = [
  143. { label: '优秀', value: 1 },
  144. { label: '合格', value: 2 },
  145. { label: '不合格', value: 3 },
  146. { label: '丧失传承能力', value: 4 },
  147. { label: '取消资格', value: 5 },
  148. ];
  149. const reviewProgress = computed(
  150. () => {
  151. const currentUserGroup = currentUserGroups.value.find((group) => GROUP_TO_REVIEW_PROGRESS[group.id]);
  152. return GROUP_TO_REVIEW_PROGRESS[currentUserGroup?.id ?? 0] ?? 0;
  153. },
  154. );
  155. const canSubmitReview = computed(
  156. () => reviewProgress.value >= 2 && reviewProgress.value <= 5,
  157. );
  158. const progressSubmitLabel = computed(
  159. () => PROGRESS_SUBMIT_LABELS[reviewProgress.value] ?? '—',
  160. );
  161. const reviewLevelLabel = computed(() => {
  162. switch (reviewProgress.value) {
  163. case 2:
  164. return '项目保护单位';
  165. case 3:
  166. return '县(区)文旅部门';
  167. case 4:
  168. return '设区市文旅部门、省非遗中心';
  169. case 5:
  170. return '省文化和旅游厅';
  171. default:
  172. return '';
  173. }
  174. });
  175. const progressHint = computed(() => {
  176. const lp = listProgress.value;
  177. const lpText = Number.isFinite(lp) ? `列表进度:${lp}` : '未传列表进度参数';
  178. const roleText = canSubmitReview.value
  179. ? `当前账号审核环节:${reviewLevelLabel.value}(提交后 progress=${reviewProgress.value})`
  180. : `当前用户组 ${currentUserGroups.value.map((group) => group.name).join(',')} 无对应审核环节`;
  181. return `${lpText}。${roleText}。`;
  182. });
  183. const currentYear = new Date().getFullYear();
  184. function levelTitleFromForm(f: SelfAssessmentDetail) {
  185. if (f.level === 23) return '国家级';
  186. if (f.level === 24) return '省级';
  187. if (f.level === 25) return '市级';
  188. return '国家级';
  189. }
  190. function loadEditorContent(f: SelfAssessmentDetail) {
  191. if (typeof f.content !== 'object' || f.content === null)
  192. f.content = {};
  193. f.content.title = `传承人填写${currentYear}年1月1日至${currentYear}年12月31日${levelTitleFromForm(f)}非遗传承人义务履行和传承补助经费使用情况等,不超过1000字,如未履行职责请进行说明。参考提纲如下:`;
  194. for (let i = 0; i < 8; i++) {
  195. if (typeof f.content[`item${i}`] !== 'string')
  196. f.content[`item${i}`] = '';
  197. }
  198. }
  199. async function loadCheckItems(f: SelfAssessmentDetail) {
  200. const { top } = await AssessmentContentApi.getCheckItems(Number(f.level));
  201. checkItemList.value = top as CheckItemInfo[];
  202. currentFormCheckItems.value = [...f.checkItems] as SelfAssessmentCheckItemAnswer[];
  203. }
  204. function applyPrefillFromDetail(f: SelfAssessmentDetail) {
  205. const t = reviewProgress.value;
  206. if (!canSubmitReview.value)
  207. return;
  208. if (t === 2) {
  209. reviewOpinion.value = f.ichUnit ?? null;
  210. reviewPoints.value = f.unitPoints || null;
  211. } else if (t === 3) {
  212. reviewOpinion.value = f.county ?? null;
  213. reviewPoints.value = f.countyPoints || null;
  214. } else if (t === 4) {
  215. reviewOpinion.value = f.district ?? null;
  216. reviewPoints.value = f.districtPoints || null;
  217. } else if (t === 5) {
  218. reviewOpinion.value = f.province ?? null;
  219. reviewPoints.value = f.provincePoints || null;
  220. }
  221. }
  222. const loader = useSimpleDataLoader(async () => {
  223. const id = queryFormId.value;
  224. const uid = queryUserId.value;
  225. if (!id || !uid) {
  226. currentForm.value = null;
  227. return null;
  228. }
  229. const detail = await AssessmentContentApi.getSelfAssessmentDetail(id, uid);
  230. currentForm.value = detail;
  231. loadEditorContent(detail);
  232. await loadCheckItems(detail);
  233. applyPrefillFromDetail(detail);
  234. return detail;
  235. }, { immediate: false });
  236. watch(reviewProgress, () => {
  237. const f = currentForm.value;
  238. if (f)
  239. applyPrefillFromDetail(f as SelfAssessmentDetail);
  240. });
  241. watch(
  242. () => [queryFormId.value, queryUserId.value, listProgress.value] as const,
  243. () => {
  244. loader.load();
  245. },
  246. );
  247. onMounted(() => {
  248. loader.load();
  249. });
  250. /** 回退后 progress:退回至上一环节 */
  251. const rejectTargetProgress = computed(() => Math.max(1, reviewProgress.value - 1));
  252. function promptRejectReason() {
  253. const draft = ref('');
  254. return new Promise<string | null>((resolve) => {
  255. Modal.confirm({
  256. title: '回退自查评估表',
  257. okText: '确认回退',
  258. cancelText: '取消',
  259. okType: 'danger',
  260. width: 800,
  261. content: () => h(Input.TextArea, {
  262. value: draft.value,
  263. rows: 4,
  264. maxlength: 500,
  265. showCount: true,
  266. placeholder: '请输入回退原因(必填)',
  267. 'onUpdate:value': (v: string) => {
  268. draft.value = v ?? '';
  269. },
  270. }),
  271. onOk: () => {
  272. const reason = draft.value.trim();
  273. if (!reason) {
  274. message.warning('请输入回退原因');
  275. return Promise.reject();
  276. }
  277. resolve(reason);
  278. },
  279. onCancel: () => {
  280. resolve(null);
  281. },
  282. });
  283. });
  284. }
  285. async function submitReject() {
  286. const f = currentForm.value;
  287. if (!f?.id) {
  288. message.warning('缺少自查表 ID');
  289. return;
  290. }
  291. if (!canSubmitReview.value) {
  292. message.warning('当前账号用户组无权回退');
  293. return;
  294. }
  295. const rejectReason = await promptRejectReason();
  296. if (rejectReason === null)
  297. return;
  298. const t = reviewProgress.value;
  299. try {
  300. rejectLoading.value = true;
  301. await AssessmentContentApi.reviewSelfAssessment({
  302. id: f.id,
  303. progress: rejectTargetProgress.value,
  304. rejectType: t,
  305. rejectReason,
  306. });
  307. message.success('已回退');
  308. router.back();
  309. } catch (e) {
  310. Modal.error({ title: '回退失败', content: formatErr(e) });
  311. } finally {
  312. rejectLoading.value = false;
  313. }
  314. }
  315. async function submitReview() {
  316. const f = currentForm.value;
  317. if (!f?.id) {
  318. message.warning('缺少自查表 ID');
  319. return;
  320. }
  321. if (!canSubmitReview.value) {
  322. message.warning('当前账号用户组无权提交审核');
  323. return;
  324. }
  325. if (reviewOpinion.value == null) {
  326. message.warning('请选择审核意见');
  327. return;
  328. }
  329. const t = reviewProgress.value;
  330. const base = { id: f.id, progress: t };
  331. const points = reviewPoints.value != null && reviewPoints.value >= 0 ? reviewPoints.value : undefined;
  332. const op = reviewOpinion.value;
  333. try {
  334. submitLoading.value = true;
  335. if (t === 2)
  336. await AssessmentContentApi.reviewSelfAssessment({ ...base, ichUnit: op, unitPoints: points });
  337. else if (t === 3)
  338. await AssessmentContentApi.reviewSelfAssessment({ ...base, county: op, countyPoints: points });
  339. else if (t === 4)
  340. await AssessmentContentApi.reviewSelfAssessment({ ...base, district: op, districtPoints: points });
  341. else
  342. await AssessmentContentApi.reviewSelfAssessment({ ...base, province: op, provincePoints: points });
  343. message.success('审核提交成功');
  344. router.back();
  345. } catch (e) {
  346. Modal.error({ title: '审核提交失败', content: formatErr(e) });
  347. } finally {
  348. submitLoading.value = false;
  349. }
  350. }
  351. </script>