argeement-sign.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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"></div>
  10. </div>
  11. <a-spin :spinning="loader.loading.value">
  12. <template v-if="!loader.loading.value">
  13. <a-result
  14. v-if="!currentAgreement"
  15. status="info"
  16. title="您还未签署传承协议"
  17. sub-title="请先签署传承协议"
  18. >
  19. <template #extra>
  20. <a-button type="primary" @click="createAgreement">去签署传承协议</a-button>
  21. </template>
  22. </a-result>
  23. <div v-else>
  24. <a-alert type="info" message="请仔细阅读传承协议,确保您已理解内容,并签署传承协议。" class="mb-4" show-icon />
  25. <a-form
  26. v-if="currentAgreement"
  27. ref="formRef"
  28. :model="currentAgreement"
  29. :rules="formRules"
  30. layout="vertical"
  31. >
  32. <a-typography-title class="mt-5!" :level="4">{{ agreementTitle }}</a-typography-title>
  33. <AgreementBodyNational
  34. v-if="currentAgreement?.level === 23"
  35. :detail="(currentAgreement as AgreementDetail)"
  36. :agreement-year="agreementYear"
  37. :party-a-stamp-date="partyAStampDate"
  38. :party-b-sign-date="partyBSignDate"
  39. @update:party-a-stamp-date="partyAStampDate = $event"
  40. @update:party-b-sign-date="partyBSignDate = $event"
  41. />
  42. <AgreementBodyProvincial
  43. v-else-if="currentAgreement?.level === 24"
  44. :detail="(currentAgreement as AgreementDetail)"
  45. :agreement-year="agreementYear"
  46. :party-a-stamp-date="partyAStampDate"
  47. :party-b-sign-date="partyBSignDate"
  48. @update:party-a-stamp-date="partyAStampDate = $event"
  49. @update:party-b-sign-date="partyBSignDate = $event"
  50. />
  51. <AgreementBodyMunicipal
  52. v-else-if="currentAgreement?.level === 25"
  53. :detail="(currentAgreement as AgreementDetail)"
  54. :agreement-year="agreementYear"
  55. :party-a-stamp-date="partyAStampDate"
  56. :party-b-sign-date="partyBSignDate"
  57. @update:party-a-stamp-date="partyAStampDate = $event"
  58. @update:party-b-sign-date="partyBSignDate = $event"
  59. />
  60. </a-form>
  61. <a-space direction="vertical" class="w-full mt-4" size="middle">
  62. <a-button type="primary" block :loading="submitLoading" @click="saveAgreement">保存传承协议</a-button>
  63. <a-button block :loading="submitLoading" @click="downloadAgreement">下载协议 PDF</a-button>
  64. </a-space>
  65. </div>
  66. </template>
  67. </a-spin>
  68. </div>
  69. </section>
  70. </div>
  71. </template>
  72. <script setup lang="ts">
  73. import { computed, h, onMounted, ref, watch } from 'vue';
  74. import { useRoute, useRouter } from 'vue-router';
  75. import { message, Modal } from 'ant-design-vue';
  76. import type { FormInstance } from 'ant-design-vue';
  77. import type { Rules } from 'async-validator';
  78. import { RequestApiError } from '@imengyu/imengyu-utils';
  79. import { useAuthStore } from '@/stores/auth';
  80. import AssessmentContentApi, { AgreementDetail } from '@/api/collect/AssessmentContent';
  81. import AgreementBodyNational from './components/AgreementBodyNational.vue';
  82. import AgreementBodyProvincial from './components/AgreementBodyProvincial.vue';
  83. import AgreementBodyMunicipal from './components/AgreementBodyMunicipal.vue';
  84. import type { AgreementYmdParts } from './components/AgreementDateWriteBlock.vue';
  85. import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
  86. import { ArrowLeftOutlined } from '@ant-design/icons-vue';
  87. import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
  88. function formatErr(e: unknown): string {
  89. if (e instanceof RequestApiError)
  90. return e.errorMessage;
  91. if (e instanceof Error)
  92. return e.message;
  93. return String(e);
  94. }
  95. const router = useRouter();
  96. const route = useRoute();
  97. const authStore = useAuthStore();
  98. const queryId = computed(() => Number(route.query.id) || 0);
  99. const queryUserId = computed(() => Number(route.query.userId) || 0);
  100. const currentAgreement = ref<AgreementDetail | null>(null);
  101. const partyAStampDate = ref<AgreementYmdParts>({ year: '', month: '', day: '' });
  102. const partyBSignDate = ref<AgreementYmdParts>({ year: '', month: '', day: '' });
  103. const formRef = ref<FormInstance | null>(null);
  104. const CN_MOBILE_RE = /^1\d{10}$/;
  105. const CN_ID_RE = /^(?:\d{15}|\d{17}[\dXx])$/;
  106. const formRules = computed<Rules>(() => {
  107. const rules: Rules = {
  108. partyB: [{ required: true, message: '请填写乙方(传承人)姓名' }],
  109. apprentice: [
  110. { required: true, message: '请填写本年度带徒人数' },
  111. { type: 'integer', min: 0, message: '须为不小于 0 的整数' },
  112. ],
  113. activity: [
  114. { required: true, message: '请填写本年度宣传活动场次' },
  115. { type: 'integer', min: 0, message: '须为不小于 0 的整数' },
  116. ],
  117. partyBSign: [
  118. {
  119. validator(_rule, value, callback) {
  120. const s = typeof value === 'string' ? value.trim() : '';
  121. if (!s)
  122. callback(new Error('请完成乙方签名'));
  123. else
  124. callback();
  125. },
  126. },
  127. ],
  128. idCard: [
  129. { required: true, message: '请填写身份证号' },
  130. {
  131. validator(_rule, value, callback) {
  132. const s = value != null ? String(value).trim() : '';
  133. if (!CN_ID_RE.test(s))
  134. callback(new Error('请输入正确的身份证号'));
  135. else
  136. callback();
  137. },
  138. },
  139. ],
  140. ich: [{ required: true, message: '请填写非遗项目名称' }],
  141. health: [{ required: true, message: '请填写身体状况' }],
  142. mobile: [
  143. { required: true, message: '请填写乙方联系电话' },
  144. {
  145. validator(_rule, value, callback) {
  146. const s = value != null ? String(value).trim() : '';
  147. if (!CN_MOBILE_RE.test(s))
  148. callback(new Error('请输入正确的手机号'));
  149. else
  150. callback();
  151. },
  152. },
  153. ],
  154. };
  155. if (currentAgreement.value?.level !== 25)
  156. rules.course = [
  157. { required: true, message: '请填写本年度研修班场次' },
  158. { type: 'integer', min: 0, message: '须为不小于 0 的整数' },
  159. ];
  160. return rules;
  161. });
  162. async function loadBasicInfo() {
  163. const uid = authStore.userInfo?.id ?? authStore.userId;
  164. const basicInfo = await AssessmentContentApi.getInheritorBasic(uid);
  165. const d = currentAgreement.value;
  166. if (!d)
  167. return;
  168. d.partyB = basicInfo.name;
  169. d.mobile = basicInfo.mobile;
  170. d.idCard = basicInfo.idCard;
  171. d.ich = basicInfo.ichName;
  172. d.level = basicInfo.level;
  173. }
  174. const agreementYear = computed(() => currentAgreement.value?.year ?? 2027);
  175. const agreementTitle = computed(
  176. () => `${agreementYear.value} 年度${levelTitle.value}非物质文化遗产代表性传承人传承协议`,
  177. );
  178. const levelTitle = computed(() => {
  179. if (currentAgreement.value?.level === 23) return '国家级';
  180. if (currentAgreement.value?.level === 24) return '省级';
  181. return '市级';
  182. });
  183. const loader = useSimpleDataLoader(async () => {
  184. if (queryId.value > 0) {
  185. const detail = await AssessmentContentApi.getAgreementDetail(queryId.value, queryUserId.value || undefined);
  186. currentAgreement.value = detail;
  187. partyAStampDate.value = { year: '', month: '', day: '' };
  188. partyBSignDate.value = { year: '', month: '', day: '' };
  189. return currentAgreement.value;
  190. }
  191. const uid = authStore.userInfo?.id ?? authStore.userId;
  192. const basicInfo = await AssessmentContentApi.getInheritorBasic(uid);
  193. if (basicInfo.agreementId > 0) {
  194. const detail = await AssessmentContentApi.getAgreementDetail(
  195. basicInfo.agreementId,
  196. uid,
  197. );
  198. currentAgreement.value = detail;
  199. partyAStampDate.value = { year: '', month: '', day: '' };
  200. partyBSignDate.value = {
  201. year: detail.updatetime.getFullYear().toString(),
  202. month: (detail.updatetime.getMonth() + 1).toString(),
  203. day: detail.updatetime.getDate().toString(),
  204. };
  205. } else {
  206. currentAgreement.value = null;
  207. }
  208. return currentAgreement.value;
  209. }, {
  210. immediate: false,
  211. });
  212. onMounted(() => {
  213. loader.load();
  214. });
  215. watch(
  216. () => [queryId.value, queryUserId.value],
  217. () => {
  218. loader.load();
  219. },
  220. );
  221. const submitLoading = ref(false);
  222. const appConfiguration = injectAppConfiguration();
  223. async function createAgreement() {
  224. const now = new Date();
  225. const u = authStore.userInfo;
  226. const nick = u?.nickname || u?.username || '';
  227. const uid = authStore.userInfo?.id ?? authStore.userId;
  228. const basicInfo = await AssessmentContentApi.getInheritorBasic(uid);
  229. const detail = new AgreementDetail();
  230. detail.userId = uid;
  231. detail.year = appConfiguration.value?.collectSignYear || now.getFullYear() + 1;
  232. detail.level = basicInfo.level;
  233. if (basicInfo.level === 24) {
  234. detail.partyA = '厦门市文化和旅游局';
  235. } else if (basicInfo.level === 25) {
  236. detail.partyA = basicInfo.regionText + '文化和旅游局';
  237. } else {
  238. detail.partyA = '福建省文化和旅游厅';
  239. }
  240. detail.partyB = nick;
  241. partyAStampDate.value = {
  242. year: now.getFullYear().toString(),
  243. month: (now.getMonth() + 1).toString(),
  244. day: now.getDate().toString(),
  245. };
  246. partyBSignDate.value = {
  247. year: now.getFullYear().toString(),
  248. month: (now.getMonth() + 1).toString(),
  249. day: now.getDate().toString(),
  250. };
  251. currentAgreement.value = detail;
  252. await loadBasicInfo();
  253. }
  254. async function saveAgreement() {
  255. try {
  256. await formRef.value?.validate();
  257. } catch {
  258. message.warning('请填写完整信息');
  259. return;
  260. }
  261. submitLoading.value = true;
  262. try {
  263. const d = currentAgreement.value;
  264. if (!d) {
  265. submitLoading.value = false;
  266. return;
  267. }
  268. await AssessmentContentApi.saveAgreement(d as AgreementDetail);
  269. message.success('保存传承协议成功');
  270. } catch (error) {
  271. Modal.error({
  272. title: '保存传承协议失败',
  273. content: formatErr(error),
  274. });
  275. }
  276. submitLoading.value = false;
  277. }
  278. async function downloadAgreement() {
  279. if (!currentAgreement.value?.id) {
  280. message.warning('请先保存传承协议后再下载 PDF');
  281. return;
  282. }
  283. try {
  284. await AssessmentContentApi.downloadAgreementPdf(currentAgreement.value.id);
  285. message.success('已开始下载');
  286. } catch (error) {
  287. Modal.error({
  288. title: '下载失败',
  289. content: formatErr(error),
  290. });
  291. }
  292. }
  293. </script>