evaluation-form.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <template>
  2. <CommonRoot>
  3. <FlexCol padding="space.lg">
  4. <SimplePageContentLoader :loader="loader">
  5. <template v-if="loader.isFinished.value">
  6. <Result
  7. v-if="!currentForm"
  8. status="info"
  9. title="您还未填写评估表"
  10. >
  11. <Height :height="30" />
  12. <Button type="primary" @click="createForm">去填写评估表</Button>
  13. </Result>
  14. <FlexCol v-else gap="gap.lg">
  15. <EvaluationFormBlock
  16. :currentForm="(currentForm as SelfAssessmentDetail)"
  17. :formOptions="formOptions"
  18. :checkItemList="(checkItemList as CheckItemInfo[])"
  19. :currentFormCheckItems="(currentFormCheckItems as SelfAssessmentCheckItemAnswer[])"
  20. />
  21. <Divider />
  22. <FlexRow align="flex-end" justify="space-between">
  23. <H3>自评总分</H3>
  24. <Text fontSize="50rpx" color="#315816" fontFamily="HUNdin1451" :text="`${totalPoints}分`" />
  25. </FlexRow>
  26. <Divider />
  27. <FlexCol gap="gap.lg">
  28. <FlexCol v-for="(title, secIdx) in externalReviewSectionTitles" :key="secIdx" gap="gap.sm">
  29. <Text bold :text="title" />
  30. <Text text="评分:(待终审填写)" />
  31. <FlexRow wrap align="center" gap="gap.md">
  32. <CheckBox
  33. v-for="(label, i) in externalReviewScoreRow1"
  34. :key="`${secIdx}-r1-${i}`"
  35. disabled
  36. :model-value="false"
  37. :text="label"
  38. :check-size="28"
  39. />
  40. </FlexRow>
  41. <FlexRow wrap align="center" gap="gap.md">
  42. <CheckBox
  43. v-for="(label, i) in externalReviewScoreRow2"
  44. :key="`${secIdx}-r2-${i}`"
  45. disabled
  46. :model-value="false"
  47. :text="label"
  48. :check-size="28"
  49. />
  50. </FlexRow>
  51. <FlexCol align="flex-end">
  52. <Text color="text.second" text="填写单位(盖章)" />
  53. <Text color="text.second" text="年 月 日" />
  54. </FlexCol>
  55. </FlexCol>
  56. </FlexCol>
  57. <Divider />
  58. <H3>佐证资料上传</H3>
  59. <Result v-if="!currentForm?.id" status="info" title="请先保存评估表后再上传佐证资料" />
  60. <Uploader
  61. v-else
  62. ref="uploaderRef"
  63. :upload="assessmentAnnexUpload"
  64. :max-upload-count="9"
  65. :max-file-size="20 * 1024 * 1024"
  66. :group-type="true"
  67. list-type="list"
  68. />
  69. <Height :height="30" />
  70. <Divider />
  71. <Button type="primary" block :loading="submitLoading" @click="saveForm">保存评估表</Button>
  72. <Button :loading="submitLoading" @click="downloadForm">下载评估表PDF</Button>
  73. </FlexCol>
  74. </template>
  75. </SimplePageContentLoader>
  76. <XBarSpace />
  77. </FlexCol>
  78. </CommonRoot>
  79. </template>
  80. <script setup lang="ts">
  81. import { computed, ref } from 'vue';
  82. import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
  83. import { useAuthStore } from '@/store/auth';
  84. import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
  85. import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
  86. import { assertNotNull, formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
  87. import { toast, alert } from '@/components/dialog/CommonRoot';
  88. import AssessmentContentApi, {
  89. SelfAssessmentDetail,
  90. CheckItemInfo,
  91. SelfAssessmentCheckItemAnswer,
  92. getCheckAnnexType,
  93. } from '@/api/collect/AssessmentContent';
  94. import CommonRoot from '@/components/dialog/CommonRoot.vue';
  95. import Button from '@/components/basic/Button.vue';
  96. import Result from '@/components/feedback/Result.vue';
  97. import FlexCol from '@/components/layout/FlexCol.vue';
  98. import Height from '@/components/layout/space/Height.vue';
  99. import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
  100. import H3 from '@/components/typography/H3.vue';
  101. import FlexRow from '@/components/layout/FlexRow.vue';
  102. import Text from '@/components/basic/Text.vue';
  103. import XBarSpace from '@/components/layout/space/XBarSpace.vue';
  104. import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
  105. import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
  106. import type { FieldProps } from '@/components/form/Field.vue';
  107. import type { SignatureFieldProps } from '@/components/form/SignatureField.vue';
  108. import { useImageSimpleUploadCo } from '@/common/components/upload/ImageUploadCo';
  109. import EvaluationFormBlock from './components/EvaluationFormBlock.vue';
  110. import Uploader, { type UploaderInstance } from '@/components/form/Uploader.vue';
  111. import { getMimeType } from '@/common/components/upload/mimes';
  112. import Divider from '@/components/display/Divider.vue';
  113. import { stringUrlToUploaderItem } from '@/components/form/Uploader';
  114. import CheckBox from '@/components/form/CheckBox.vue';
  115. /** 评估表下方展示用:各级审核意见(不接数据、禁用) */
  116. const externalReviewSectionTitles = [
  117. '1. 项目保护单位意见',
  118. '2. 县(区)文旅部门审核意见',
  119. '3. 设区市文旅部门、省非遗中心审核意见',
  120. ] as const;
  121. const externalReviewScoreRow1 = ['优秀', '合格', '不合格'] as const;
  122. const externalReviewScoreRow2 = ['丧失传承能力', '取消资格'] as const;
  123. let loaded = false;
  124. const { querys } = useLoadQuerys({
  125. id: 0,
  126. userId: 0,
  127. }, () => {
  128. if (loaded)
  129. return;
  130. loaded = true;
  131. loader.load();
  132. });
  133. const currentForm = ref<SelfAssessmentDetail | null>(null);
  134. const currentFormCheckItems = ref<SelfAssessmentCheckItemAnswer[]>([]);
  135. const authStore = useAuthStore();
  136. const assessmentAnnexUpload = useAliOssUploadCo('assessment/annex', async (res, item) => {
  137. assertNotNull(currentForm.value, 'currentForm is null');
  138. const mimetype = getMimeType(item.filePath);
  139. await AssessmentContentApi.saveAnnex({
  140. name: '佐证资料' + StringUtils.path.getFileName(item.filePath),
  141. formId: currentForm.value.id,
  142. url: res,
  143. type: getCheckAnnexType(mimetype),
  144. mimetype: mimetype,
  145. fileSize: item.size
  146. ? Math.max(1, Math.ceil(item.size / 1024))
  147. : undefined,
  148. });
  149. });
  150. const uploaderRef = ref<UploaderInstance | null>(null);
  151. const formRef = ref<IDynamicFormRef | null>(null);
  152. const formOptions : IDynamicFormOptions = {
  153. formAdditionaProps: {
  154. labelFlex: 4,
  155. inputFlex: 8,
  156. },
  157. formItems: [
  158. {
  159. type: 'flat-group',
  160. label: '传承人自查评估',
  161. name: 'selfAssessmentGroup',
  162. childrenColProps: { span: 24 },
  163. children: [
  164. {
  165. label: '传承人名称',
  166. name: 'inheritor',
  167. type: 'text',
  168. additionalProps: { placeholder: '请输入传承人名称' },
  169. },
  170. {
  171. label: '项目保护单位',
  172. name: 'unit',
  173. type: 'text',
  174. additionalProps: { placeholder: '请输入项目保护单位' },
  175. },
  176. {
  177. label: '项目名称',
  178. name: 'ichName',
  179. type: 'text',
  180. additionalProps: { placeholder: '请输入项目名称' },
  181. },
  182. {
  183. label: '联系电话',
  184. name: 'mobile',
  185. type: 'text',
  186. additionalProps: { placeholder: '请输入联系电话' },
  187. },
  188. {
  189. label: '身份证号',
  190. name: 'idCard',
  191. type: 'text',
  192. additionalProps: { placeholder: '请输入身份证号' },
  193. },
  194. {
  195. label: '级别',
  196. name: 'level',
  197. type: 'select-id',
  198. additionalProps: {
  199. placeholder: '请选择级别',
  200. loadData: async () => [
  201. { text: '国家级', value: 23 },
  202. { text: '省级', value: 24 },
  203. { text: '市级', value: 25 },
  204. ],
  205. },
  206. formProps: {
  207. showRightArrow: true,
  208. },
  209. },
  210. {
  211. label: '家庭住址',
  212. name: 'address',
  213. type: 'text',
  214. additionalProps: { placeholder: '请输入家庭住址' },
  215. },
  216. {
  217. label: '获评时间',
  218. name: 'awardTime',
  219. type: 'date',
  220. additionalProps: {
  221. placeholder: '请选择获评时间',
  222. shouldUpdateValueImmediately: true,
  223. },
  224. formProps: {
  225. showRightArrow: true,
  226. },
  227. },
  228. {
  229. label: '自评报告',
  230. name: 'content',
  231. type: 'textarea',
  232. additionalProps: {
  233. placeholder: '请填写自评报告',
  234. rows: 10,
  235. showWordLimit: true,
  236. maxLength: 3000,
  237. multiline: true,
  238. confirmType: "return",
  239. } as FieldProps,
  240. },
  241. ],
  242. },
  243. {
  244. type: 'insertion',
  245. name: 'insertCheckList',
  246. },
  247. {
  248. type: 'flat-group',
  249. label: '传承人自查评估',
  250. name: 'selfAssessmentGroup2',
  251. childrenColProps: { span: 24 },
  252. children: [
  253. {
  254. label: '其他相关情况(扣分内容)',
  255. name: 'deductContent',
  256. type: 'text',
  257. additionalProps: {
  258. showWordLimit: true,
  259. maxlength: 260,
  260. placeholder: '请输入其他相关情况(扣分内容)',
  261. } as FieldProps,
  262. },
  263. {
  264. label: '其他相关情况(扣分分值)',
  265. name: 'deductPoints',
  266. type: 'number',
  267. additionalProps: {
  268. placeholder: '请输入其他相关情况(扣分分值)',
  269. min: 0,
  270. max: 100,
  271. },
  272. },
  273. {
  274. label: '自我评估',
  275. name: 'self',
  276. type: 'radio-value',
  277. additionalProps: {
  278. options: [
  279. { text: '优秀', value: 1 },
  280. { text: '合格', value: 2 },
  281. { text: '不合格', value: 3 },
  282. { text: '丧失传承能力', value: 4 },
  283. { text: '取消资格', value: 5 },
  284. ],
  285. vertical: true,
  286. } as RadioValueProps,
  287. },
  288. {
  289. label: '传承人签名',
  290. name: 'sign',
  291. type: 'sign',
  292. formProps: {
  293. showRightArrow: true,
  294. },
  295. additionalProps: {
  296. upload: useImageSimpleUploadCo(),
  297. previewImageProps: {
  298. width: '400rpx',
  299. }
  300. } as SignatureFieldProps,
  301. }
  302. ],
  303. },
  304. ],
  305. formRules: {
  306. inheritor: [{ required: true, message: '请输入传承人名称' }],
  307. unit: [{ required: true, message: '请输入项目保护单位' }],
  308. ichName: [{ required: true, message: '请输入项目名称' }],
  309. mobile: [{ required: true, message: '请输入联系电话' }],
  310. idCard: [{ required: true, message: '请输入身份证号' }],
  311. level: [{ required: true, message: '请选择级别' }],
  312. address: [{ required: true, message: '请输入家庭住址' }],
  313. content: [{ required: true, message: '请填写自评报告' }],
  314. self: [{ required: true, message: '请选择自我评估' }],
  315. sign: [{ required: true, message: '请传承人签名' }],
  316. awardTime: [{ required: true, message: '请选择获评时间' }],
  317. },
  318. };
  319. const checkItemList = ref<CheckItemInfo[]>([]);
  320. let checkItemMap = new Map<number, CheckItemInfo>();
  321. const totalPoints = computed(() => {
  322. if (!currentForm.value)
  323. return 0;
  324. return currentFormCheckItems.value
  325. .filter((item) => {
  326. const checkItem = checkItemMap.get(item.id);
  327. return checkItem && !checkItem.isTitle;
  328. })
  329. .reduce((acc, item) => acc + (item.count * item.points), 0)
  330. - (currentForm.value.deductPoints ?? 0);
  331. });
  332. async function loadBasicInfo() {
  333. const basicInfo = await AssessmentContentApi.getInheritorBasic(authStore.userInfo?.id);
  334. assertNotNull(currentForm.value, 'currentForm is null');
  335. currentForm.value.inheritor = basicInfo.name;
  336. currentForm.value.unit = basicInfo.unit;
  337. currentForm.value.ichName = basicInfo.ichName;
  338. currentForm.value.mobile = basicInfo.mobile;
  339. currentForm.value.level = basicInfo.level;
  340. currentForm.value.idCard = basicInfo.idCard;
  341. currentForm.value.address = basicInfo.address;
  342. }
  343. async function loadCheckItems() {
  344. assertNotNull(currentForm.value, 'currentForm is null');
  345. const { top, map } = await AssessmentContentApi.getCheckItems(Number(currentForm.value.level));
  346. checkItemList.value = top;
  347. checkItemMap = map;
  348. currentFormCheckItems.value = currentForm.value.checkItems.concat();
  349. }
  350. async function loadAnnexList() {
  351. assertNotNull(currentForm.value, 'currentForm is null');
  352. console.log('awardTime', currentForm.value.awardTime);
  353. const annexList = await AssessmentContentApi.getAnnexList(currentForm.value.id);
  354. setTimeout(() => {
  355. uploaderRef.value!.setList(annexList.data.map((item) => stringUrlToUploaderItem(item.url)));
  356. }, 1000);
  357. }
  358. const submitLoading = ref(false);
  359. async function createForm() {
  360. const detail = new SelfAssessmentDetail();
  361. detail.userId = authStore.userInfo!.id;
  362. detail.year = new Date().getFullYear();
  363. detail.checkItems = [];
  364. currentForm.value = detail;
  365. await loadBasicInfo();
  366. await loadCheckItems();
  367. }
  368. async function saveForm() {
  369. const detail = currentForm.value;
  370. try {
  371. await formRef.value?.validate();
  372. } catch (error) {
  373. toast('请填写完整信息');
  374. return;
  375. }
  376. submitLoading.value = true;
  377. currentForm.value!.checkItems = currentFormCheckItems.value;
  378. try {
  379. assertNotNull(detail, 'currentForm is null');
  380. await AssessmentContentApi.saveSelfAssessment(detail as SelfAssessmentDetail);
  381. toast('保存评估表成功');
  382. await waitTimeOut(1000);
  383. await loader.reload();
  384. } catch (error) {
  385. alert({
  386. title: '保存评估表失败',
  387. content: formatError(error),
  388. });
  389. }
  390. submitLoading.value = false;
  391. }
  392. async function downloadForm() {
  393. if (!currentForm.value?.id) {
  394. toast('请先保存评估表后再下载PDF');
  395. return;
  396. }
  397. try {
  398. assertNotNull(currentForm.value, 'currentForm is null');
  399. const pdfPath = await AssessmentContentApi.downloadSelfAssessmentPdf(currentForm.value.id);
  400. uni.openDocument({
  401. filePath: pdfPath,
  402. fileType: 'pdf',
  403. showMenu: true,
  404. });
  405. } catch (error) {
  406. alert({
  407. title: '下载评估表失败',
  408. content: formatError(error),
  409. });
  410. }
  411. }
  412. const loader = useSimpleDataLoader(async () => {
  413. await waitTimeOut(1000);
  414. if (querys.value.id > 0) {
  415. const detail = await AssessmentContentApi.getSelfAssessmentDetail(querys.value.id, querys.value.userId);
  416. currentForm.value = detail;
  417. await loadCheckItems();
  418. await loadAnnexList();
  419. return;
  420. }
  421. const currentYear = new Date().getFullYear();
  422. const list = await AssessmentContentApi.getSelfAssessmentList({
  423. userId: authStore.userInfo?.id,
  424. year: currentYear,
  425. });
  426. if (list.data.length > 0) {
  427. const currentYearItem = list.data.find((item) => item.year === currentYear);
  428. const detail = await AssessmentContentApi.getSelfAssessmentDetail(
  429. currentYearItem?.id || list.data[0].id,
  430. currentYearItem?.userId || authStore.userInfo?.id
  431. );
  432. currentForm.value = detail;
  433. await loadAnnexList();
  434. await loadCheckItems();
  435. } else {
  436. currentForm.value = null;
  437. }
  438. return currentForm.value;
  439. }, false);
  440. </script>