argeement-sign-review.vue 7.6 KB

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