evaluation-form.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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 gap-2">
  7. <a-button :icon="h(ArrowLeftOutlined)" @click="router.back()">返回</a-button>
  8. <h2>自查评估表</h2>
  9. <div class="w-20"></div>
  10. </div>
  11. <a-spin :spinning="loader.loading.value">
  12. <a-result
  13. v-if="!currentForm"
  14. status="info"
  15. title="您还未填写评估表"
  16. >
  17. <template #extra>
  18. <a-button type="primary" @click="createForm">去填写评估表</a-button>
  19. </template>
  20. </a-result>
  21. <div v-else>
  22. <a-alert
  23. v-if="authStore.isAdmin"
  24. type="info"
  25. show-icon
  26. class="mb-4"
  27. >
  28. <template #message>
  29. 本页用于管理员手动修改内容,如果您是审核者,请点击<a href="javascript:void(0)" @click="reviewerView">这里审核</a>。
  30. </template>
  31. </a-alert>
  32. <SelfAssessmentFormDisplay
  33. ref="blockRef"
  34. :current-form="(currentForm as SelfAssessmentDetail)"
  35. :check-item-list="(checkItemList as CheckItemInfo[])"
  36. :current-form-check-items="(currentFormCheckItems as SelfAssessmentCheckItemAnswer[])"
  37. :readonly="false"
  38. />
  39. <template v-if="isReviewer && currentForm">
  40. <a-image v-if="currentForm.scan" :src="currentForm.scan" class="small-preview" alt="avatar" />
  41. <a-upload
  42. class="flex flex-row items-center gap-2 mt-2"
  43. list-type="picture-card"
  44. :show-upload-list="false"
  45. :custom-request="(options: any) => handleScanUpload(options)"
  46. >
  47. <UploadOutlined /> 上传
  48. </a-upload>
  49. <a-divider />
  50. <a-button type="primary" block :loading="submitLoading" @click="reviewModalVisible = true">过会后编辑分数和意见</a-button>
  51. </template>
  52. <a-divider />
  53. <a-space direction="vertical" class="w-full" size="middle">
  54. <a-button type="primary" block :loading="submitLoading" @click="saveForm">保存评估表</a-button>
  55. <a-button v-if="!authStore.isAdmin && currentForm?.progress === 0" type="primary" block :loading="submitLoading" @click="submitForm">提交审核</a-button>
  56. <div class="flex flex-row gap-2">
  57. <a-button block @click="previewForm">预览评估表 PDF</a-button>
  58. <a-button block :loading="submitLoading" @click="downloadForm">下载评估表 PDF</a-button>
  59. </div>
  60. </a-space>
  61. </div>
  62. </a-spin>
  63. </div>
  64. </section>
  65. <a-modal
  66. title="预览评估表 PDF"
  67. v-model:visible="previewVisible"
  68. width="80%"
  69. :footer="null"
  70. >
  71. <iframe :src="previewUrl" class="w-full h-full" style="min-height: 60vh"></iframe>
  72. </a-modal>
  73. <a-modal
  74. title="过会后编辑分数和意见"
  75. v-model:visible="reviewModalVisible"
  76. :confirm-loading="reviewSubmitting"
  77. ok-text="提交审核"
  78. cancel-text="取消"
  79. @ok="handleReviewSubmit"
  80. >
  81. <a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
  82. <a-form-item label="审核角色" required>
  83. <a-select v-model:value="reviewForm.groupId" placeholder="请选择审核角色">
  84. <a-select-option v-for="opt in groupOptions" :key="opt.value" :value="Number(opt.value)">{{ opt.label }}</a-select-option>
  85. </a-select>
  86. </a-form-item>
  87. <a-form-item label="审核操作" required>
  88. <a-radio-group v-model:value="reviewForm.rejectType">
  89. <a-radio :value="0">通过</a-radio>
  90. <a-radio :value="rejectTypeForGroup">退回</a-radio>
  91. </a-radio-group>
  92. </a-form-item>
  93. <a-form-item v-if="reviewForm.rejectType === 0" label="评估级别">
  94. <a-select v-model:value="reviewForm.opinion" placeholder="请选择评估级别">
  95. <a-select-option v-for="opt in opinionOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
  96. </a-select>
  97. </a-form-item>
  98. <a-form-item v-if="reviewForm.rejectType === 0" label="评分">
  99. <a-input-number v-model:value="reviewForm.points" :min="0" :max="100" class="w-full" placeholder="请输入评分" />
  100. </a-form-item>
  101. <a-form-item v-if="reviewForm.rejectType > 0" label="退回意见">
  102. <a-textarea v-model:value="reviewForm.rejectReason" :rows="3" :maxlength="500" show-count placeholder="请输入退回意见" />
  103. </a-form-item>
  104. <a-form-item label="发送短信">
  105. <a-switch v-model:checked="reviewSendMsg" />
  106. </a-form-item>
  107. </a-form>
  108. </a-modal>
  109. </div>
  110. </template>
  111. <script setup lang="ts">
  112. import { computed, h, onMounted, ref, watch } from 'vue';
  113. import { useRoute, useRouter } from 'vue-router';
  114. import { message, Modal, type UploadProps } from 'ant-design-vue';
  115. import { UploadOutlined } from '@ant-design/icons-vue';
  116. import { StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
  117. import { RequestApiError } from '@imengyu/imengyu-utils';
  118. import { ArrowLeftOutlined } from '@ant-design/icons-vue';
  119. import { useAuthStore } from '@/stores/auth';
  120. import AssessmentContentApi, {
  121. SelfAssessmentDetail,
  122. CheckItemInfo,
  123. SelfAssessmentCheckItemAnswer,
  124. type SelfAssessmentReviewOnlyPayload,
  125. } from '@/api/collect/AssessmentContent';
  126. import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vue';
  127. import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
  128. import { injectAppConfiguration } from '@/api/system/useAppConfiguration.ts';
  129. import { getFormErrorFieldsMessage } from '@/common/Form.ts';
  130. import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
  131. import { useReview } from './composeables/Review.ts';
  132. import { GROUP_LABELS, GROUP_TO_REVIEW_PROGRESS } from './composeables/GroupData.ts';
  133. import CommonContent from '@/api/CommonContent.ts';
  134. function formatErr(e: unknown): string {
  135. if (e instanceof RequestApiError)
  136. return e.errorMessage;
  137. if (e instanceof Error)
  138. return e.message;
  139. return String(e);
  140. }
  141. const router = useRouter();
  142. const route = useRoute();
  143. const authStore = useAuthStore();
  144. const appConfiguration = injectAppConfiguration();
  145. const queryId = computed(() => Number(route.query.id) || 0);
  146. const queryUserId = computed(() => Number(route.query.userId) || 0);
  147. const currentForm = ref<SelfAssessmentDetail | null>(null);
  148. const currentFormCheckItems = ref([] as SelfAssessmentCheckItemAnswer[]);
  149. const checkItemList = ref([] as CheckItemInfo[]);
  150. const currentProgress = computed(() => currentForm.value?.progress ?? 0);
  151. const { currentUserGroup, isDistrict, isEndLevel, isProtectUnit } = useReview(currentProgress);
  152. const isReviewer = computed(() => isDistrict.value || isEndLevel.value || isProtectUnit.value);
  153. const blockRef = ref<InstanceType<typeof SelfAssessmentFormDisplay> | null>(null);
  154. const submitLoading = ref(false);
  155. const levelTitle = computed(() => {
  156. if (currentForm.value?.level === LEVEL_NATIONAL) return '国家级';
  157. if (currentForm.value?.level === LEVEL_PROVINCE) return '省级';
  158. if (currentForm.value?.level === LEVEL_DISTRICT) return '市级';
  159. return '国家级';
  160. });
  161. function loadEditorContent() {
  162. if (!currentForm.value)
  163. return;
  164. if (typeof currentForm.value.content !== 'object' || currentForm.value.content === null)
  165. currentForm.value.content = {};
  166. currentForm.value.content.title = `传承人填写${currentForm.value.year}年1月1日至${currentForm.value.year}年12月31日${levelTitle.value}非遗传承人义务履行和传承补助经费使用情况等,不超过1000字,如未履行职责请进行说明。参考提纲如下:`;
  167. for (let i = 0; i < 8; i++) {
  168. if (typeof currentForm.value.content[`item${i}`] !== 'string')
  169. currentForm.value.content[`item${i}`] = '';
  170. }
  171. }
  172. async function loadBasicInfo() {
  173. const uid = authStore.userInfo?.id ?? authStore.userId;
  174. const basicInfo = await AssessmentContentApi.getInheritorBasic(uid);
  175. const f = currentForm.value;
  176. if (!f)
  177. return;
  178. f.inheritor = basicInfo.name;
  179. f.unit = basicInfo.unit;
  180. f.ichName = basicInfo.ichName;
  181. f.mobile = basicInfo.mobile;
  182. f.level = basicInfo.level;
  183. f.idCard = basicInfo.idCard;
  184. f.address = basicInfo.address;
  185. }
  186. async function loadCheckItems() {
  187. const f = currentForm.value;
  188. if (!f)
  189. return;
  190. const { top } = await AssessmentContentApi.getCheckItems(Number(f.level));
  191. checkItemList.value = top as CheckItemInfo[];
  192. currentFormCheckItems.value = [...f.checkItems] as SelfAssessmentCheckItemAnswer[];
  193. }
  194. async function createForm() {
  195. const uid = authStore.userInfo?.id ?? authStore.userId;
  196. const detail = new SelfAssessmentDetail();
  197. detail.userId = uid;
  198. detail.year = appConfiguration.value?.collectFormYear || new Date().getFullYear();
  199. detail.checkItems = [];
  200. currentForm.value = detail;
  201. loadEditorContent();
  202. await loadBasicInfo();
  203. await loadCheckItems();
  204. }
  205. async function saveForm() {
  206. const cf = currentForm.value;
  207. submitLoading.value = true;
  208. if (!cf) {
  209. submitLoading.value = false;
  210. return;
  211. }
  212. if (cf.progress > 0) {
  213. try {
  214. await blockRef.value?.validate();
  215. } catch (e: unknown) {
  216. message.warning('请填写完整信息: ' + getFormErrorFieldsMessage(e));
  217. submitLoading.value = false;
  218. return;
  219. }
  220. }
  221. /* if (cf.progress > 0) {
  222. const confirmed = await new Promise<boolean>((resolve) => {
  223. Modal.confirm({
  224. title: '提示',
  225. content: '您之前已提交审核,修改将导致审核撤回并需要重新审核,是否继续修改?',
  226. okText: '继续修改',
  227. cancelText: '取消',
  228. onOk: () => resolve(true),
  229. onCancel: () => resolve(false),
  230. });
  231. });
  232. if (!confirmed)
  233. return;
  234. } */
  235. cf.checkItems = currentFormCheckItems.value;
  236. try {
  237. await AssessmentContentApi.saveSelfAssessment(cf as SelfAssessmentDetail/* , 0 */);
  238. message.success('保存评估表成功');
  239. await waitTimeOut(500);
  240. await loader.load();
  241. } catch (error) {
  242. Modal.error({
  243. title: '保存评估表失败',
  244. content: formatErr(error),
  245. });
  246. }
  247. submitLoading.value = false;
  248. }
  249. async function submitForm() {
  250. try {
  251. await blockRef.value?.validate();
  252. } catch (e: unknown) {
  253. message.warning('请填写完整信息: ' + getFormErrorFieldsMessage(e));
  254. return;
  255. }
  256. const confirmed = await new Promise<boolean>((resolve) => {
  257. Modal.confirm({
  258. title: '提示',
  259. content: '您确认要提交审核吗?请确认各项信息填写无误。',
  260. okText: '确认提交',
  261. cancelText: '取消',
  262. onOk: () => resolve(true),
  263. onCancel: () => resolve(false),
  264. });
  265. });
  266. if (!confirmed)
  267. return;
  268. submitLoading.value = true;
  269. const cf = currentForm.value;
  270. if (!cf) {
  271. submitLoading.value = false;
  272. return;
  273. }
  274. cf.checkItems = currentFormCheckItems.value;
  275. try {
  276. await AssessmentContentApi.saveSelfAssessment(cf as SelfAssessmentDetail, 1);
  277. message.success('提交审核成功');
  278. await waitTimeOut(500);
  279. await loader.load();
  280. } catch (error) {
  281. Modal.error({
  282. title: '提交审核失败',
  283. content: formatErr(error),
  284. });
  285. }
  286. submitLoading.value = false;
  287. }
  288. const previewVisible = ref(false);
  289. const previewUrl = ref('');
  290. function revokePreviewUrl() {
  291. if (previewUrl.value) {
  292. URL.revokeObjectURL(previewUrl.value);
  293. previewUrl.value = '';
  294. }
  295. }
  296. async function previewForm() {
  297. if (!currentForm.value?.id) {
  298. message.warning('请先保存评估表后再预览 PDF');
  299. return;
  300. }
  301. try {
  302. previewVisible.value = true;
  303. revokePreviewUrl();
  304. previewUrl.value = await AssessmentContentApi.previewSelfAssessmentPdf(currentForm.value.id);
  305. } catch (error) {
  306. Modal.error({
  307. title: '预览评估表失败',
  308. content: formatErr(error),
  309. });
  310. }
  311. }
  312. watch(previewVisible, (visible) => {
  313. if (!visible) {
  314. revokePreviewUrl();
  315. }
  316. });
  317. async function downloadForm() {
  318. if (!currentForm.value?.id) {
  319. message.warning('请先保存评估表后再下载 PDF');
  320. return;
  321. }
  322. try {
  323. await AssessmentContentApi.downloadSelfAssessmentPdf(currentForm.value.id);
  324. message.success('已开始下载');
  325. } catch (error) {
  326. Modal.error({
  327. title: '下载评估表失败',
  328. content: formatErr(error),
  329. });
  330. }
  331. }
  332. function reviewerView() {
  333. router.push({
  334. name: 'CollectEvaluationFormReview',
  335. query: {
  336. id: queryId.value,
  337. userId: queryUserId.value,
  338. ...(currentForm.value?.progress != null && currentForm.value?.progress !== undefined ? { progress: String(currentForm.value?.progress) } : {}),
  339. },
  340. });
  341. }
  342. const reviewModalVisible = ref(false);
  343. const reviewSubmitting = ref(false);
  344. const reviewSendMsg = ref(false);
  345. const reviewForm = ref({
  346. groupId: Number(currentUserGroup.value?.id ?? 0),
  347. rejectType: 0,
  348. opinion: undefined as number | undefined,
  349. points: undefined as number | undefined,
  350. rejectReason: '',
  351. });
  352. const groupOptions = computed(() => {
  353. return Object.entries(GROUP_LABELS)
  354. .filter(([key]) => Number(key) > 0)
  355. .map(([key, label]) => ({ value: key, label }));
  356. });
  357. const opinionOptions = [
  358. { label: '优秀', value: 1 },
  359. { label: '合格', value: 2 },
  360. { label: '不合格', value: 3 },
  361. { label: '丧失传承能力', value: 4 },
  362. { label: '取消资格', value: 5 },
  363. ];
  364. const rejectTypeForGroup = computed(() => {
  365. const gid = Number(reviewForm.value.groupId);
  366. return GROUP_TO_REVIEW_PROGRESS[gid]?.rejectTarget || 0;
  367. });
  368. async function handleScanUpload(options: Parameters<NonNullable<UploadProps['customRequest']>>[0]) {
  369. const { file, onSuccess, onError } = options;
  370. try {
  371. const res = await CommonContent.uploadSmallFile(file as File, 'image', 'file');
  372. onSuccess?.({
  373. url: res.fullurl,
  374. name: StringUtils.path.getFileName(res.fullurl),
  375. });
  376. currentForm.value!.scan = res.fullurl;
  377. } catch (err) {
  378. onError?.(err as Error);
  379. message.error(err instanceof Error ? err.message : String(err));
  380. }
  381. }
  382. async function handleReviewSubmit() {
  383. const cf = currentForm.value;
  384. if (!cf?.id) {
  385. message.warning('请先保存评估表');
  386. return;
  387. }
  388. if (!reviewForm.value.groupId) {
  389. message.warning('请选择审核角色');
  390. return;
  391. }
  392. reviewSubmitting.value = true;
  393. try {
  394. const payload: SelfAssessmentReviewOnlyPayload = {
  395. checkId: cf.id,
  396. groupId: reviewForm.value.groupId,
  397. rejectType: reviewForm.value.rejectType,
  398. rejectReason: reviewForm.value.rejectType > 0 ? reviewForm.value.rejectReason : undefined,
  399. opinion: reviewForm.value.rejectType === 0 ? reviewForm.value.opinion : undefined,
  400. points: reviewForm.value.rejectType === 0 ? reviewForm.value.points : undefined,
  401. sendMsg: reviewSendMsg.value ? 1 : 0,
  402. };
  403. await AssessmentContentApi.reviewOnly(payload);
  404. message.success('审核提交成功');
  405. reviewModalVisible.value = false;
  406. reviewForm.value = { groupId: currentUserGroup.value?.id ?? 0, rejectType: 0, opinion: undefined, points: undefined, rejectReason: '' };
  407. reviewSendMsg.value = false;
  408. await loader.load();
  409. } catch (error) {
  410. Modal.error({
  411. title: '审核提交失败',
  412. content: formatErr(error),
  413. });
  414. }
  415. reviewSubmitting.value = false;
  416. }
  417. const loader = useSimpleDataLoader(async () => {
  418. if (queryId.value > 0) {
  419. const detail = await AssessmentContentApi.getSelfAssessmentDetail(queryId.value, queryUserId.value || undefined);
  420. currentForm.value = detail;
  421. loadEditorContent();
  422. await loadCheckItems();
  423. return currentForm.value;
  424. }
  425. const uid = authStore.userInfo?.id ?? authStore.userId;
  426. const basicInfo = await AssessmentContentApi.getInheritorBasic(uid);
  427. if (basicInfo.checkId > 0) {
  428. const detail = await AssessmentContentApi.getSelfAssessmentDetail(basicInfo.checkId, uid);
  429. currentForm.value = detail;
  430. console.log(currentForm.value);
  431. loadEditorContent();
  432. await loadCheckItems();
  433. } else {
  434. currentForm.value = null;
  435. }
  436. return currentForm.value;
  437. }, {
  438. immediate: false,
  439. });
  440. onMounted(() => {
  441. loader.load();
  442. });
  443. watch(
  444. () => [queryId.value, queryUserId.value],
  445. () => {
  446. loader.load();
  447. },
  448. );
  449. </script>
  450. <style scoped>
  451. .total-points {
  452. font-size: 1.75rem;
  453. color: #315816;
  454. font-weight: 600;
  455. }
  456. </style>