Forráskód Böngészése

💊 按要求佐证资料改为与选项绑定

快乐的梦鱼 1 hónapja
szülő
commit
aa60e37d94

+ 4 - 1
src/api/collect/AssessmentContent.ts

@@ -558,6 +558,7 @@ export interface SaveCheckAnnexPayload {
   id?: number;
   name: string;
   formId: number;
+  itemId?: number;
   url: string;
   type: CheckAnnexTypeValue | number;
   desc?: string;
@@ -795,6 +796,7 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
       id: payload.id,
       name: payload.name,
       form_id: payload.formId,
+      item_id: payload.itemId,
       url: payload.url,
       type: payload.type,
       desc: payload.desc,
@@ -804,9 +806,10 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
     }, '证明材料保存');
   }
 
-  async getAnnexList(formId: number) {
+  async getAnnexList(formId: number, itemId?: number) {
     const res = await this.post('/ich/check/getAnnexList', {
       form_id: formId,
+      item_id: itemId,
     }, '证明材料列表');
     return normalizePaginated<CheckAnnexListItem>(CheckAnnexListItem, res.data as KeyValue);
   }

+ 221 - 1
src/pages/collect/assessment/components/EvaluationFormBlock.vue

@@ -65,6 +65,51 @@
           {{ child.name }} ({{ child.points }}分)
         </a-checkbox>
       </template>
+      <div class="annex-upload-block">
+        <a-typography-text type="secondary">{{ readonly ? '佐证资料' : '佐证材料上传' }}</a-typography-text>
+        <a-alert
+          v-if="!currentForm.id"
+          type="info"
+          class="mt-2"
+          message="请先保存评估表后再上传佐证资料"
+          show-icon
+        />
+        <template v-else-if="readonly">
+          <a-empty v-if="!getAnnexFileList(item.id).length" description="暂无佐证资料" />
+          <ul v-else class="annex-list pl-5 mt-2">
+            <li v-for="file in getAnnexFileList(item.id)" :key="file.uid">
+              <a :href="file.url" target="_blank" rel="noopener noreferrer">({{ (file.response as any).desc ?? '暂无说明' }}){{ file.name }}</a>
+            </li>
+          </ul>
+        </template>
+        <a-upload
+          v-else
+          class="flex flex-row items-center gap-2 mt-2"
+          :file-list="getAnnexFileList(item.id)"
+          :multiple="true"
+          :custom-request="(options: any) => annexCustomRequest(item.id, options)"
+          :before-upload="beforeAnnexUpload"
+          @update:file-list="(list: any) => setAnnexFileList(item.id, list)"
+        >
+          <template #itemRender="{ originNode, file }">
+            <div class="flex flex-row items-center gap-2">
+              <div class="flex-1 min-w-0">
+                <component :is="originNode" />
+              </div>
+              <a-button
+                type="link"
+                size="small"
+                class="mt-2!"
+                :disabled="file?.status !== 'done'"
+                @click.prevent.stop="editAnnexDesc(item.id, file)"
+              >
+                编辑
+              </a-button>
+            </div>
+          </template>
+          <a-button type="default" size="small" class="ml-3">选择文件上传</a-button>
+        </a-upload>
+      </div>
     </div>
     <a-divider />
     <DynamicForm
@@ -76,16 +121,21 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue';
+import { computed, h, ref, watch } from 'vue';
 import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
 import { ArrayUtils } from '@imengyu/imengyu-utils';
+import type { UploadProps } from 'ant-design-vue';
+import { Input, message, Modal } from 'ant-design-vue';
 import type { FormInstance } from 'ant-design-vue';
 import {
+  getCheckAnnexType,
+  default as AssessmentContentApi,
   SelfAssessmentCheckItemAnswer,
   type CheckItemInfo,
   type SelfAssessmentDetail,
 } from '@/api/collect/AssessmentContent';
 import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
+import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 import type { RadioValueFormItemProps, SelectIdProps } from '@imengyu/vue-dynamic-form-ant';
 import type { SignProps } from '@imengyu/vue-dynamic-form-rich';
 
@@ -101,6 +151,8 @@ const props = withDefaults(defineProps<{
 
 const mainFormRef = ref<IDynamicFormRef | null>(null);
 const tailFormRef = ref<IDynamicFormRef | null>(null);
+const annexUploadCo = useAliOssUploadCo('assessment/annex');
+const annexFileMap = ref<Record<number, UploadProps['fileList']>>({});
 
 const contentItemLabels = [
   '(一)开展传承活动,培养后继人才情况;',
@@ -333,6 +385,174 @@ function setCheckedItem(checkItem: CheckItemInfo, childItem: CheckItemInfo, coun
     ArrayUtils.remove(props.currentFormCheckItems, item);
 }
 
+function getAnnexFileList(itemId: number) {
+  return annexFileMap.value[itemId] ?? [];
+}
+
+function setAnnexFileList(itemId: number, list: UploadProps['fileList']) {
+  annexFileMap.value[itemId] = list ?? [];
+}
+
+function formatAnnexDisplayName(desc: string | null | undefined, name: string) {
+  const descText = (desc ?? '').trim();
+  if (!descText)
+    return name;
+  return `${descText}(${name})`;
+}
+
+function mapAnnexListItemToUploadFile(item: any) {
+  const rawName = String(item.name ?? '');
+  const desc = String(item.desc ?? '');
+  return {
+    uid: String(item.id),
+    name: formatAnnexDisplayName(desc, rawName),
+    status: 'done' as const,
+    url: item.url,
+    response: {
+      ...item,
+      rawName,
+      desc,
+    },
+  };
+}
+
+const beforeAnnexUpload: UploadProps['beforeUpload'] = () => {
+  if (!props.currentForm.id) {
+    message.warning('请先保存评估表');
+    return false;
+  }
+  return true;
+};
+
+async function loadAnnexListByItem(itemId: number) {
+  const formId = props.currentForm.id;
+  if (!formId) {
+    setAnnexFileList(itemId, []);
+    return;
+  }
+  const annexList = await AssessmentContentApi.getAnnexList(formId, itemId);
+  setAnnexFileList(itemId, annexList.data.map(mapAnnexListItemToUploadFile));
+}
+
+function promptAnnexDesc(initialDesc: string, title: string) {
+  const draft = ref(initialDesc);
+  return new Promise<string | null>((resolve) => {
+    Modal.confirm({
+      title,
+      okText: '保存',
+      cancelText: '取消',
+      content: () => h(Input.TextArea, {
+        value: draft.value,
+        rows: 4,
+        maxlength: 200,
+        showCount: true,
+        placeholder: '请输入说明(可选)',
+        'onUpdate:value': (v: string) => {
+          draft.value = v ?? '';
+        },
+      }),
+      onOk: () => {
+        resolve(draft.value.trim());
+      },
+      onCancel: () => {
+        resolve(null);
+      },
+    });
+  });
+}
+
+async function editAnnexDesc(itemId: number, file: any, isAfterUpload = false) {
+  const formId = props.currentForm.id;
+  if (!formId)
+    return;
+  const res = file?.response ?? {};
+  const annexId = Number(res.id ?? file.uid);
+  const rawName = String(res.rawName ?? res.name ?? file.name ?? '').trim();
+  const url = String(res.url ?? file.url ?? '').trim();
+  if (!annexId || !rawName || !url) {
+    message.warning('附件信息不完整,暂无法编辑说明');
+    return;
+  }
+  const currentDesc = String(res.desc ?? '').trim();
+  const nextDesc = await promptAnnexDesc(
+    currentDesc,
+    isAfterUpload ? '上传成功,请补充佐证资料说明' : '编辑佐证资料说明',
+  );
+  if (nextDesc === null)
+    return;
+  await AssessmentContentApi.saveAnnex({
+    id: annexId,
+    name: rawName,
+    formId,
+    itemId,
+    url,
+    type: Number(res.type ?? 5),
+    desc: nextDesc,
+    mimetype: res.mimetype,
+    attachId: res.attachId,
+    fileSize: res.fileSize,
+  });
+  await loadAnnexListByItem(itemId);
+}
+
+async function annexCustomRequest(itemId: number, options: Parameters<NonNullable<UploadProps['customRequest']>>[0]) {
+  const { file, onSuccess, onError } = options;
+  try {
+    const formId = props.currentForm.id;
+    if (!formId)
+      return;
+    const raw = file as File;
+    const mimetype = raw.type || 'application/octet-stream';
+    let uploadedUrl = '';
+    await new Promise<void>((resolve, reject) => {
+      annexUploadCo.uploadRequest({
+        action: 'upload',
+        filename: raw.name,
+        data: {},
+        headers: {},
+        file: raw,
+        withCredentials: false,
+        method: 'POST',
+        onProgress: () => {},
+        onSuccess: (body: unknown) => {
+          uploadedUrl = annexUploadCo.getUrlByUploadResponse?.(body) ?? '';
+          resolve();
+        },
+        onError: (err) => reject(err),
+      });
+    });
+    await AssessmentContentApi.saveAnnex({
+      name: raw.name,
+      formId,
+      itemId,
+      url: uploadedUrl,
+      type: getCheckAnnexType(mimetype),
+      mimetype,
+      fileSize: raw.size ? Math.max(1, Math.ceil(raw.size / 1024)) : undefined,
+    });
+    onSuccess?.({ url: uploadedUrl }, raw as never);
+    await loadAnnexListByItem(itemId);
+    const uploadedFile = getAnnexFileList(itemId).find((f: any) => {
+      const fileUrl = String(f?.response?.url ?? f?.url ?? '');
+      return fileUrl === uploadedUrl;
+    });
+    if (uploadedFile)
+      await editAnnexDesc(itemId, uploadedFile, true);
+  } catch (err) {
+    onError?.(err as Error);
+    message.error(err instanceof Error ? err.message : String(err));
+  }
+}
+
+watch(
+  () => [props.currentForm.id, props.checkItemList.map((item) => item.id).join(',')],
+  async () => {
+    const itemIds = props.checkItemList.map((item) => item.id);
+    await Promise.all(itemIds.map(loadAnnexListByItem));
+  },
+  { immediate: true },
+);
+
 async function validate() {
   if (props.readonly)
     return;

+ 0 - 28
src/pages/collect/assessment/evaluation-form-review.vue

@@ -29,14 +29,6 @@
               :readonly="true"
             />
             <a-divider />
-            <a-typography-title :level="4">佐证资料</a-typography-title>
-            <a-empty v-if="!annexLinks.length" description="暂无佐证资料" />
-            <ul v-else class="annex-list pl-5">
-              <li v-for="a in annexLinks" :key="a.id">
-                <a :href="a.url" target="_blank" rel="noopener noreferrer">{{ a.name }}</a>
-              </li>
-            </ul>
-            <a-divider />
             <a-card title="审核提交" size="small">
               <a-alert
                 v-if="!canSubmitReview"
@@ -126,7 +118,6 @@ const listProgress = computed(() => {
 const currentForm = ref<SelfAssessmentDetail | null>(null);
 const currentFormCheckItems = ref([] as SelfAssessmentCheckItemAnswer[]);
 const checkItemList = ref([] as CheckItemInfo[]);
-const annexLinks = ref<{ id: number; name: string; url: string }[]>([]);
 
 const submitLoading = ref(false);
 const reviewOpinion = ref<number | null>(null);
@@ -219,15 +210,6 @@ async function loadCheckItems(f: SelfAssessmentDetail) {
   currentFormCheckItems.value = [...f.checkItems] as SelfAssessmentCheckItemAnswer[];
 }
 
-async function loadAnnexLinks(formId: number) {
-  const annexList = await AssessmentContentApi.getAnnexList(formId);
-  annexLinks.value = annexList.data.map((item) => ({
-    id: item.id,
-    name: item.name,
-    url: item.url,
-  }));
-}
-
 function applyPrefillFromDetail(f: SelfAssessmentDetail) {
   const t = reviewProgress.value;
   if (!canSubmitReview.value)
@@ -258,10 +240,6 @@ const loader = useSimpleDataLoader(async () => {
   currentForm.value = detail;
   loadEditorContent(detail);
   await loadCheckItems(detail);
-  if (detail.id)
-    await loadAnnexLinks(detail.id);
-  else
-    annexLinks.value = [];
   applyPrefillFromDetail(detail);
   return detail;
 }, { immediate: false });
@@ -320,9 +298,3 @@ async function submitReview() {
   }
 }
 </script>
-
-<style scoped>
-.annex-list {
-  list-style: disc;
-}
-</style>

+ 0 - 91
src/pages/collect/assessment/evaluation-form.vue

@@ -40,24 +40,6 @@
               <div class="text-end text-secondary mt-2 text-sm">填写单位(盖章) 年 月 日</div>
             </div>
             <a-divider />
-            <a-typography-title :level="4">佐证资料上传</a-typography-title>
-            <a-alert
-              v-if="!currentForm?.id"
-              type="info"
-              message="请先保存评估表后再上传佐证资料"
-              class="mb-4"
-              show-icon
-            />
-            <a-upload
-              v-else
-              v-model:file-list="annexFileList"
-              :multiple="true"
-              :custom-request="annexCustomRequest"
-              :before-upload="beforeAnnexUpload"
-            >
-              <a-button type="default">选择文件上传</a-button>
-            </a-upload>
-            <a-divider />
             <a-space direction="vertical" class="w-full" size="middle">
               <a-button type="primary" block :loading="submitLoading" @click="saveForm">保存评估表</a-button>
               <a-button block :loading="submitLoading" @click="downloadForm">下载评估表 PDF</a-button>
@@ -73,17 +55,14 @@
 import { computed, h, onMounted, ref, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { message, Modal } from 'ant-design-vue';
-import type { UploadProps } from 'ant-design-vue';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
 import { RequestApiError } from '@imengyu/imengyu-utils';
 import { ArrowLeftOutlined } from '@ant-design/icons-vue';
 import { useAuthStore } from '@/stores/auth';
-import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 import AssessmentContentApi, {
   SelfAssessmentDetail,
   CheckItemInfo,
   SelfAssessmentCheckItemAnswer,
-  getCheckAnnexType,
 } from '@/api/collect/AssessmentContent';
 import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vue';
 import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
@@ -120,10 +99,6 @@ const externalReviewScoreRow2 = ['丧失传承能力', '取消资格'] as const;
 
 const currentYear = new Date().getFullYear();
 
-const annexFileList = ref<UploadProps['fileList']>([]);
-
-const annexUploadCo = useAliOssUploadCo('assessment/annex');
-
 const levelTitle = computed(() => {
   if (currentForm.value?.level === 23) return '国家级';
   if (currentForm.value?.level === 24) return '省级';
@@ -168,19 +143,6 @@ async function loadCheckItems() {
   currentFormCheckItems.value = [...f.checkItems] as SelfAssessmentCheckItemAnswer[];
 }
 
-async function loadAnnexList() {
-  if (!currentForm.value?.id)
-    return;
-  const annexList = await AssessmentContentApi.getAnnexList(currentForm.value.id);
-  annexFileList.value = annexList.data.map((item) => ({
-    uid: String(item.id),
-    name: item.name,
-    status: 'done' as const,
-    url: item.url,
-    response: item,
-  }));
-}
-
 async function createForm() {
   const uid = authStore.userInfo?.id ?? authStore.userId;
   const detail = new SelfAssessmentDetail();
@@ -191,7 +153,6 @@ async function createForm() {
   loadEditorContent();
   await loadBasicInfo();
   await loadCheckItems();
-  annexFileList.value = [];
 }
 
 async function saveForm() {
@@ -243,63 +204,12 @@ async function downloadForm() {
   }
 }
 
-const beforeAnnexUpload: UploadProps['beforeUpload'] = () => {
-  if (!currentForm.value?.id) {
-    message.warning('请先保存评估表');
-    return false;
-  }
-  return true;
-};
-
-const annexCustomRequest: UploadProps['customRequest'] = async (options) => {
-  const { file, onSuccess, onError } = options;
-  try {
-    const cf = currentForm.value;
-    if (!cf)
-      return;
-    const raw = file as File;
-    const mimetype = raw.type || 'application/octet-stream';
-    let uploadedUrl = '';
-    await new Promise<void>((resolve, reject) => {
-      annexUploadCo.uploadRequest({
-        action: 'upload',
-        filename: raw.name,
-        data: {},
-        headers: {},
-        file: raw,
-        withCredentials: false,
-        method: 'POST',
-        onProgress: (progress) => {},
-        onSuccess: (body: unknown) => {
-          uploadedUrl = annexUploadCo.getUrlByUploadResponse?.(body) ?? '';
-          resolve();
-        },
-        onError: (err) => reject(err),
-      });
-    });
-    await AssessmentContentApi.saveAnnex({
-      name: raw.name,
-      formId: cf.id,
-      url: uploadedUrl,
-      type: getCheckAnnexType(mimetype),
-      mimetype,
-      fileSize: raw.size ? Math.max(1, Math.ceil(raw.size / 1024)) : undefined,
-    });
-    onSuccess?.({ url: uploadedUrl }, raw as never);
-    await loadAnnexList();
-  } catch (err) {
-    onError?.(err as Error);
-    message.error(formatErr(err));
-  }
-};
-
 const loader = useSimpleDataLoader(async () => {
   if (queryId.value > 0) {
     const detail = await AssessmentContentApi.getSelfAssessmentDetail(queryId.value, queryUserId.value || undefined);
     currentForm.value = detail;
     loadEditorContent();
     await loadCheckItems();
-    await loadAnnexList();
     return currentForm.value;
   }
   const uid = authStore.userInfo?.id ?? authStore.userId;
@@ -309,7 +219,6 @@ const loader = useSimpleDataLoader(async () => {
     currentForm.value = detail;
     console.log(currentForm.value);
     loadEditorContent();
-    await loadAnnexList();
     await loadCheckItems();
   } else {
     currentForm.value = null;