argeement-sign-review.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <template>
  2. <div class="about main-background main-background-type0">
  3. <div v-if="!isInMiniProgram" 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. <div
  13. v-if="loader.loading.value" class="h-50"
  14. />
  15. <a-result
  16. v-else-if="!currentAgreement"
  17. status="warning"
  18. title="无法加载传承协议"
  19. sub-title="请从管理员列表进入,并确认传承协议 ID 与传承人用户 ID 正确。"
  20. />
  21. <div v-else>
  22. <a-alert
  23. type="info"
  24. show-icon
  25. class="mb-4"
  26. :message="progressHint"
  27. />
  28. <AgreementFormDisplay
  29. ref="agreementFormRef"
  30. :currentAgreement="(currentAgreement as AgreementDetail)"
  31. :isProtectUnit="isProtectUnit"
  32. :isEndLevel="isEndLevel"
  33. isReviewer
  34. />
  35. <a-divider />
  36. <a-card title="审核提交" size="small" class="review-submit-card" :class="{ collapsed: reviewCardCollapsed }">
  37. <template #extra>
  38. <a-button
  39. size="small"
  40. type="text"
  41. :icon="h(reviewCardCollapsed ? UpOutlined : DownOutlined)"
  42. @click="reviewCardCollapsed = !reviewCardCollapsed"
  43. >
  44. {{ reviewCardCollapsed ? '展开' : '收起' }}
  45. </a-button>
  46. </template>
  47. <a-alert
  48. v-if="!canSubmitReview"
  49. type="warning"
  50. show-icon
  51. class="mb-4"
  52. message="当前账号用户组无权在此环节审核"
  53. />
  54. <a-alert
  55. v-else-if="currentAgreement.rejectType > 0"
  56. type="warning"
  57. show-icon
  58. class="mb-4"
  59. :message="`${getRejectTypeText(currentAgreement.rejectType)}已退回: ${currentAgreement.rejectReason}`"
  60. />
  61. <a-form v-else layout="vertical" size="middle">
  62. <div class="flex flex-col md:flex-row lg:flex-row w-full gap-3">
  63. <div class="flex flex-col flex-1">
  64. <a-form-item required :label="isNoProgressChange ? `提交意见:${reviewLevelLabel}` : `审核通过:${reviewLevelLabel}`">
  65. <a-button
  66. block
  67. type="primary"
  68. :loading="submitLoading"
  69. :disabled="!canSubmitReview"
  70. @click="submitReview"
  71. >
  72. {{ isNoProgressChange ? '提交意见' : '通过审核' }}
  73. </a-button>
  74. </a-form-item>
  75. </div>
  76. <div v-if="!isNoProgressChange || reviewProgressInfo?.canReject" class="flex flex-col flex-1">
  77. <a-form-item required label="不通过回退:回退原因">
  78. <a-textarea
  79. v-model:value="rejectReason"
  80. allow-clear
  81. placeholder="请输入回退原因"
  82. rows="3"
  83. maxlength="500"
  84. show-count
  85. />
  86. </a-form-item>
  87. <a-button
  88. block
  89. danger
  90. :loading="rejectLoading"
  91. :disabled="!canSubmitReview"
  92. @click="submitReject"
  93. >
  94. 回退
  95. </a-button>
  96. </div>
  97. <div v-else class="flex flex-col flex-1">
  98. <h3>当前用户不能回退</h3>
  99. </div>
  100. </div>
  101. </a-form>
  102. </a-card>
  103. </div>
  104. </a-spin>
  105. </div>
  106. </section>
  107. </div>
  108. </template>
  109. <script setup lang="ts">
  110. import { computed, h, onMounted, ref, watch } from 'vue';
  111. import { useRoute, useRouter } from 'vue-router';
  112. import { message, Modal } from 'ant-design-vue';
  113. import { RequestApiError } from '@imengyu/imengyu-utils';
  114. import { ArrowLeftOutlined, UpOutlined, DownOutlined } from '@ant-design/icons-vue';
  115. import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
  116. import { useMemorizeVar } from '@/composeables/useMemorizeVar';
  117. import { isInMiniProgram } from '@/composeables/MiniProgramIng.ts';
  118. import AssessmentContentApi, { AgreementDetail, getRejectTypeText } from '@/api/collect/AssessmentContent';
  119. import AgreementFormDisplay from './components/AgreementFormDisplay.vue';
  120. import { useReview } from './composeables/Review.ts';
  121. import { getFormErrorFieldsMessage } from '@/common/Form.ts';
  122. function formatErr(e: unknown): string {
  123. if (e instanceof RequestApiError)
  124. return e.errorMessage;
  125. if (e instanceof Error)
  126. return e.message;
  127. return String(e);
  128. }
  129. const router = useRouter();
  130. const route = useRoute();
  131. const agreementFormRef = ref<InstanceType<typeof AgreementFormDisplay> | null>(null);
  132. const queryFormId = computed(() => Number(route.query.id) || 0);
  133. const queryUserId = computed(() => Number(route.query.userId) || 0);
  134. const queryProgress = computed(() => {
  135. const p = Number(route.query.progress);
  136. return Number.isFinite(p) ? p : 0;
  137. });
  138. const currentAgreement = ref<AgreementDetail | null>(null);
  139. const { variable: reviewCardCollapsed } = useMemorizeVar<boolean>('argeement-sign-review-card-collapsed', false);
  140. const submitLoading = ref(false);
  141. const rejectLoading = ref(false);
  142. const rejectReason = ref<string | null>(null);
  143. const currentProgress = computed(() => currentAgreement.value?.progress ?? 0);
  144. const { reviewProgressInfo, canSubmitReview, reviewLevelLabel, progressHint, isEndLevel, isProtectUnit, isNoProgressChange, canRejectOnly } = useReview(currentProgress);
  145. const loader = useSimpleDataLoader(async () => {
  146. const id = queryFormId.value;
  147. const uid = queryUserId.value;
  148. if (!id || !uid) {
  149. currentAgreement.value = null;
  150. return null;
  151. }
  152. const detail = await AssessmentContentApi.getAgreementDetail(id, uid);
  153. currentAgreement.value = detail;
  154. return detail;
  155. }, { immediate: false });
  156. watch(
  157. () => [queryFormId.value, queryUserId.value] as const,
  158. () => { loader.load(); },
  159. );
  160. onMounted(() => { loader.load(); });
  161. async function submitReview() {
  162. const d = currentAgreement.value;
  163. if (!d?.id) {
  164. message.warning('缺少传承协议 ID');
  165. return;
  166. }
  167. if (!canSubmitReview.value) {
  168. message.warning('当前账号用户组无权提交审核');
  169. return;
  170. }
  171. try {
  172. await agreementFormRef.value?.validate();
  173. } catch (e) {
  174. message.warning('请填写完整信息: ' + getFormErrorFieldsMessage(e));
  175. return;
  176. }
  177. try {
  178. submitLoading.value = true;
  179. await AssessmentContentApi.saveAgreement(d as AgreementDetail);
  180. if (!reviewProgressInfo.value?.noProgressChange) {
  181. await AssessmentContentApi.reviewAgreement({
  182. id: d.id,
  183. progress: reviewProgressInfo.value.target,
  184. });
  185. }
  186. message.success(reviewProgressInfo.value?.noProgressChange ? '意见提交成功' : '审核通过');
  187. loader.load();
  188. } catch (e) {
  189. Modal.error({ title: '审核提交失败', content: formatErr(e) });
  190. } finally {
  191. submitLoading.value = false;
  192. }
  193. }
  194. async function submitReject() {
  195. const d = currentAgreement.value;
  196. if (!d?.id) {
  197. message.warning('缺少传承协议 ID');
  198. return;
  199. }
  200. if (!canSubmitReview.value) {
  201. message.warning('当前账号用户组无权回退');
  202. return;
  203. }
  204. const reason = (rejectReason.value ?? '').trim();
  205. if (!reason) {
  206. message.warning('请输入回退原因');
  207. return;
  208. }
  209. try {
  210. rejectLoading.value = true;
  211. await AssessmentContentApi.reviewAgreement({
  212. id: d.id,
  213. progress: queryProgress.value,
  214. rejectType: reviewProgressInfo.value.rejectTarget,
  215. rejectReason: reason,
  216. });
  217. message.success('已回退');
  218. loader.load();
  219. } catch (e) {
  220. Modal.error({ title: '回退失败', content: formatErr(e) });
  221. } finally {
  222. rejectLoading.value = false;
  223. }
  224. }
  225. </script>
  226. <style lang="scss" scoped>
  227. .review-submit-card {
  228. position: sticky;
  229. bottom: 0;
  230. z-index: 10;
  231. box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
  232. :deep(.ant-form-item) {
  233. margin-bottom: 0!important;
  234. }
  235. &.collapsed :deep(.ant-card-body) {
  236. display: none;
  237. }
  238. }
  239. </style>