Bläddra i källkod

📦 按要求修改0717会议问题(前端)

快乐的梦鱼 1 månad sedan
förälder
incheckning
c509e9824c

+ 3 - 0
src/api/collect/AssessmentConsts.ts

@@ -0,0 +1,3 @@
+export const LEVEL_NATIONAL = 23;
+export const LEVEL_PROVINCE = 24;
+export const LEVEL_DISTRICT = 25;

+ 49 - 3
src/api/collect/AssessmentContent.ts

@@ -834,13 +834,15 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
     super();
   }
 
-  async getDataCount() {
+  async getDataCount(level?: number) {
     const res = (await this.post<{
       agreement: object;
       agreement_reject: object;
       check: object;
       check_reject: object;
-    }>('/ich/check/dataCount', '自查/协议数据统计', {})).requireData();
+    }>('/ich/check/dataCount', '自查/协议数据统计', {
+      level,
+    })).requireData();
     return {
       /**自查评估表统计 {进度值progress:数量} */
       check: transformArrayDataModel<DataCountItem>(DataCountItem, transformSomeToArray(res.check), 'data.check'),
@@ -1049,10 +1051,49 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
   async downloadSelfAssessmentPdf(id: number) {
     await downloadPdfBlob('/pdf/create', id, this.config.baseUrl, `self-assessment-${id}.pdf`);
   }
+  /**
+   * 预览自查评估表 PDF
+   */
+  async previewSelfAssessmentPdf(id: number) {
+    const url = `${this.config.baseUrl}/pdf/create/${id}?main_body_id=${ApiCofig.mainBodyId}`;
+    const auth = useAuthStore();
+    const res = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        token: auth.token,
+        __token__: auth.token,
+      },
+      body: JSON.stringify({ id }),
+    });
+    if (!res.ok)
+      throw new Error('预览失败,状态码:' + res.status);
+    const blob = await res.blob();
+    return URL.createObjectURL(blob);
+  }
 
   async downloadAgreementPdf(id: number) {
     await downloadPdfBlob('/pdf/nationalContract', id, this.config.baseUrl, `agreement-${id}.pdf`);
   }
+  /**
+   * 预览传承协议 PDF
+   */
+  async previewAgreementPdf(id: number) {
+    const url = `${this.config.baseUrl}/pdf/nationalContract/?id=${id}&main_body_id=${ApiCofig.mainBodyId}`;
+    const auth = useAuthStore();
+    const res = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        token: auth.token,
+        __token__: auth.token,
+      },
+    });
+    if (!res.ok)
+      throw new Error('预览失败,状态码:' + res.status);
+    const blob = await res.blob();
+    return URL.createObjectURL(blob);
+  }
 
   async saveAnnex(payload: SaveCheckAnnexPayload) {
     return this.post('/ich/check/saveAnnex', '证明材料保存', {
@@ -1134,12 +1175,17 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
    * 打包传承人自查表、汇总表zip下载
    * @see https://www.showdoc.com.cn/minnanCE/11559060626887137
    */
-  async downloadCheckZip(data: { year?: number; progress?: number }) {
+  async downloadCheckZip(data: { 
+    year?: number; 
+    level?: number;
+    progress?: number 
+  }) {
     const auth = useAuthStore();
     let url = `${this.config.baseUrl}/inheritor_check_download/index`;
     url = appendGetUrlParams(url, 'main_body_id', ApiCofig.mainBodyId);
     url = appendGetUrlParams(url, 'token', auth.token);
     if (data.year) url = appendGetUrlParams(url, 'year', String(data.year));
+    if (data.level !== undefined) url = appendGetUrlParams(url, 'level', String(data.level));
     if (data.progress !== undefined) url = appendGetUrlParams(url, 'progress', String(data.progress));
     const res = await fetch(url, {
       method: 'POST',

+ 9 - 1
src/components/content/CommonListBlock.vue

@@ -403,7 +403,15 @@ watch(route, () => {
 defineExpose({
   reload() {
     newsLoader.load(true);
-  }
+  },
+  setDropDownValues(values: number[]) {
+    dropDownValues.value = values;
+    newsLoader.load(true);
+  },
+  setDropDownValue(value: number, index: number) {
+    dropDownValues.value[index] = value;
+    newsLoader.load(true);
+  },
 })
 </script>
 

+ 4 - 3
src/pages/admin.vue

@@ -176,6 +176,7 @@ import { InfoCircleOutlined } from '@ant-design/icons-vue';
 import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
 import { useMemorizeVar } from '@/composeables/useMemorizeVar';
 import { isInMiniProgram } from '@/composeables/MiniProgramIng.ts';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
 
 const { toClipboard } = useClipboard();
 const router = useRouter();
@@ -209,9 +210,9 @@ const { variable: lastAgreementLevel } = useMemorizeVar('adminUserAgreementLevel
 
 const selfAssessmentLevelOptions: DropdownCommonItem[] = [
   { id: 0, name: '全部等级' },
-  { id: 23, name: '国家级' },
-  { id: 24, name: '省级' },
-  { id: 25, name: '市级' },
+  { id: LEVEL_NATIONAL, name: '国家级' },
+  { id: LEVEL_PROVINCE, name: '省级' },
+  { id: LEVEL_DISTRICT, name: '市级' },
 ];
 
 const computedCategoryOptions = computed<DropdownCommonItem[]>(() => {

+ 4 - 3
src/pages/collect/assessment/argeement-sign-list.vue

@@ -98,6 +98,7 @@ use([CanvasRenderer, PieChart, BarChart, LineChart, TitleComponent, TooltipCompo
 import VChart from "vue-echarts";
 import CommonListBlock, { type DropdownCommonItem } from '@/components/content/CommonListBlock.vue';
 import AssessmentContentApi, { type UserAgreementListRow } from '@/api/collect/AssessmentContent';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts';
 
 const dataStats = ref<Awaited<ReturnType<typeof AssessmentContentApi.getDataCount>>|null>(null);
 
@@ -170,9 +171,9 @@ const { variable: lastAgreementLevel } = useMemorizeVar('adminUserAgreementLevel
 
 const agreementLevelOptions: DropdownCommonItem[] = [
   { id: 0, name: '全部等级' },
-  { id: 23, name: '国家级' },
-  { id: 24, name: '省级' },
-  { id: 25, name: '市级' },
+  { id: LEVEL_NATIONAL, name: '国家级' },
+  { id: LEVEL_PROVINCE, name: '省级' },
+  { id: LEVEL_DISTRICT, name: '市级' },
 ];
 
 const agreementProgressOptions: DropdownCommonItem[] = [

+ 46 - 3
src/pages/collect/assessment/argeement-sign.vue

@@ -52,13 +52,23 @@
               <a-space direction="vertical" class="w-full mt-4" size="middle">
                 <a-button type="primary" block :loading="submitLoading" @click="saveAgreement">保存传承协议</a-button>
                 <a-button v-if="true || !authStore.isAdmin && currentAgreement?.progress === 0" type="primary" block :loading="submitLoading" @click="submitForm">提交审核</a-button>
-                <a-button block :loading="submitLoading" @click="downloadAgreement">下载协议 PDF</a-button>
+                <div class="flex flex-row gap-2">
+                  <a-button block @click="previewAgreement">预览协议 PDF</a-button>
+                  <a-button block :loading="submitLoading" @click="downloadAgreement">下载协议 PDF</a-button>
+                </div>
               </a-space>
             </div>
           </template>
         </a-spin>
       </div>
     </section>
+    <a-modal
+      title="预览传承协议 PDF"
+      v-model:visible="previewVisible"
+      width="80%"
+    >
+      <iframe :src="previewUrl" class="w-full h-full" style="min-height: 60vh"></iframe>
+    </a-modal>
   </div>
 </template>
 
@@ -71,10 +81,10 @@ import { useAuthStore } from '@/stores/auth';
 import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
 import { ArrowLeftOutlined } from '@ant-design/icons-vue';
 import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
-import AgreementFormDisplay from './components/AgreementFormDisplay.vue';
-import AssessmentContentApi, { AgreementDetail } from '@/api/collect/AssessmentContent';
 import { isInMiniProgram } from '@/composeables/MiniProgramIng.ts';
 import { getFormErrorFieldsMessage } from '@/common/Form.ts';
+import AgreementFormDisplay from './components/AgreementFormDisplay.vue';
+import AssessmentContentApi, { AgreementDetail } from '@/api/collect/AssessmentContent';
 
 function formatErr(e: unknown): string {
   if (e instanceof RequestApiError)
@@ -250,6 +260,39 @@ async function submitForm() {
 }
 
 
+const previewVisible = ref(false);
+const previewUrl = ref('');
+
+function revokePreviewUrl() {
+  if (previewUrl.value) {
+    URL.revokeObjectURL(previewUrl.value);
+    previewUrl.value = '';
+  }
+}
+
+async function previewAgreement() {
+  if (!currentAgreement.value?.id) {
+    message.warning('请先保存传承协议后再预览 PDF');
+    return;
+  }
+  try {
+    previewVisible.value = true;
+    revokePreviewUrl();
+    previewUrl.value = await AssessmentContentApi.previewAgreementPdf(currentAgreement.value.id);
+  } catch (error) {
+    Modal.error({
+      title: '预览传承协议失败',
+      content: formatErr(error),
+    });
+  }
+}
+
+watch(previewVisible, (visible) => {
+  if (!visible) {
+    revokePreviewUrl();
+  }
+});
+
 async function downloadAgreement() {
   if (!currentAgreement.value?.id) {
     message.warning('请先保存传承协议后再下载 PDF');

+ 9 - 7
src/pages/collect/assessment/components/AgreementFormDisplay.vue

@@ -10,19 +10,19 @@
       <a-typography-title class="mt-5!" :level="4">{{ agreementTitle }}</a-typography-title>
 
       <AgreementBodyNational
-        v-if="currentAgreement?.level === 23"
+        v-if="currentAgreement?.level === LEVEL_NATIONAL"
         :detail="(currentAgreement as AgreementDetail)"
         :agreement-year="agreementYear"
         :isReviewer="isReviewer"
       />
       <AgreementBodyProvincial
-        v-else-if="currentAgreement?.level === 24"
+        v-else-if="currentAgreement?.level === LEVEL_PROVINCE"  
         :detail="(currentAgreement as AgreementDetail)"
         :agreement-year="agreementYear"
         :isReviewer="isReviewer"
       />
       <AgreementBodyMunicipal
-        v-else-if="currentAgreement?.level === 25"
+        v-else-if="currentAgreement?.level === LEVEL_DISTRICT"
         :detail="(currentAgreement as AgreementDetail)"
         :agreement-year="agreementYear"
         :isReviewer="isReviewer"
@@ -50,7 +50,7 @@
         <a-form-item label="甲方电话" name="partyAMobile">
           <a-input v-model:value="currentAgreement.partyAMobile" :placeholder="isReviewer ? '请填写联系电话' : '待审核人员填写'" :disabled="!isReviewer" />
         </a-form-item>
-        <a-form-item label="保护单位签名" name="unitSign">
+        <a-form-item label="保护单位(审核员)签名" name="unitSign">
           <a-alert v-if="isProtectUnit && !currentAgreement.unitSign" type="info" message="你是保护单位审核,应该进行签名" show-icon></a-alert>
           <Sign 
             v-if="isReviewer || currentAgreement.unitSign" :model-value="currentAgreement.unitSign ?? ''" 
@@ -106,6 +106,7 @@ import { computed, ref } from 'vue';
 import type { FormInstance } from 'ant-design-vue';
 import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo.ts';
 import type { Rules } from 'async-validator';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
 
 const props = withDefaults(defineProps<{
   currentAgreement: AgreementDetail;
@@ -123,9 +124,10 @@ const agreementTitle = computed(
   () => `${agreementYear.value} 年度${levelTitle.value}非物质文化遗产代表性传承人传承协议`,
 );
 const levelTitle = computed(() => {
-  if (props.currentAgreement?.level === 23) return '国家级';
-  if (props.currentAgreement?.level === 24) return '省级';
-  return '市级';
+  if (props.currentAgreement?.level === LEVEL_NATIONAL) return '国家级';
+  if (props.currentAgreement?.level === LEVEL_PROVINCE) return '省级';
+  if (props.currentAgreement?.level === LEVEL_DISTRICT) return '市级';
+  return '';
 });
 
 const formRef = ref<FormInstance | null>(null);

+ 4 - 3
src/pages/collect/assessment/components/SelfAssessmentFormBlock.vue

@@ -151,6 +151,7 @@ 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';
+import { LEVEL_DISTRICT, LEVEL_NATIONAL, LEVEL_PROVINCE } from '@/api/collect/AssessmentConsts';
 
 const isDev = import.meta.env.MODE === 'development';
 
@@ -242,9 +243,9 @@ const mainFormOptionsBase: IDynamicFormOptions = {
           additionalProps: {
             placeholder: '请选择级别',
             loadData: async () => [
-              { text: '国家级', value: 23 },
-              { text: '省级', value: 24 },
-              { text: '市级', value: 25 },
+              { text: '国家级', value: LEVEL_NATIONAL },
+              { text: '省级', value: LEVEL_PROVINCE },
+              { text: '市级', value: LEVEL_DISTRICT },
             ],
           } as SelectIdProps<any>,
         },

+ 17 - 12
src/pages/collect/assessment/components/SelfAssessmentResultBlock.vue

@@ -51,6 +51,7 @@
 import { computed } from 'vue';
 import type { SelfAssessmentDetail } from '@/api/collect/AssessmentContent';
 import { GROUP_PROTECT_UNIT, GROUP_DISTRICT, GROUP_DISTRICT_CENTER, GROUP_DISTRICT_ST, GROUP_DISTRICT_ND, GROUP_DISTRICT_RD, GROUP_DISTRICT_TH, GROUP_DISTRICT_FI, GROUP_PROVINCE } from '../composeables/GroupData';
+import { LEVEL_NATIONAL } from '@/api/collect/AssessmentConsts';
 
 type OpinionField = 'ichUnit' | 'county' | 'district' | 'province' | 'districtSt' | 'districtNd' | 'districtRd' | 'districtTh' | 'districtFi';
 type PointsField = 'unitPoints' | 'countyPoints' | 'districtPoints' | 'provincePoints' | 'districtStPoints' | 'districtNdPoints' | 'districtRdPoints' | 'districtThPoints' | 'districtFiPoints';
@@ -75,17 +76,21 @@ function getOpinionLabel(value: number | null | undefined) {
   return opinionOptions.find(o => o.value === value)?.label ?? '';
 }
 
-const sections: { key: string; title: string; opinionField: OpinionField; pointsField: PointsField; rejectTypeValue: number; groupId: number }[] = [
-  { key: 'ichUnit', title: '1. 项目保护单位意见', opinionField: 'ichUnit', pointsField: 'unitPoints', rejectTypeValue: 2, groupId: GROUP_PROTECT_UNIT },
-  { key: 'county', title: '2. 县(区)文旅部门审核意见', opinionField: 'county', pointsField: 'countyPoints', rejectTypeValue: 3, groupId: GROUP_DISTRICT },
-  { key: 'district', title: '3. 设区市文旅部门、省非遗中心审核意见', opinionField: 'district', pointsField: 'districtPoints', rejectTypeValue: 4, groupId: GROUP_DISTRICT_CENTER },
-  { key: 'districtSt', title: '3.1 项目负责人意见', opinionField: 'districtSt', pointsField: 'districtStPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_ST },
-  { key: 'districtNd', title: '3.2 部门主任意见', opinionField: 'districtNd', pointsField: 'districtNdPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_ND },
-  { key: 'districtRd', title: '3.3 分管领导意见', opinionField: 'districtRd', pointsField: 'districtRdPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_RD },
-  { key: 'districtTh', title: '4.1 工服处意见', opinionField: 'districtTh', pointsField: 'districtThPoints', rejectTypeValue: 4, groupId: GROUP_DISTRICT_TH },
-  { key: 'districtFi', title: '4.2 工服处-分管领导意见', opinionField: 'districtFi', pointsField: 'districtFiPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_FI },
-  { key: 'province', title: '5. 省文化和旅游厅意见', opinionField: 'province', pointsField: 'provincePoints', rejectTypeValue: 5, groupId: GROUP_PROVINCE },
-];
+const sections = computed<{ key: string; title: string; opinionField: string; pointsField: string; rejectTypeValue: number; groupId: number }[]>(() => {
+  const result = [
+    { key: 'ichUnit', title: '1. 项目保护单位意见', opinionField: 'ichUnit', pointsField: 'unitPoints', rejectTypeValue: 2, groupId: GROUP_PROTECT_UNIT },
+    { key: 'county', title: '2. 县(区)文旅部门审核意见', opinionField: 'county', pointsField: 'countyPoints', rejectTypeValue: 3, groupId: GROUP_DISTRICT },
+    { key: 'district', title: '3. 设区市文旅部门、省非遗中心审核意见', opinionField: 'district', pointsField: 'districtPoints', rejectTypeValue: 4, groupId: GROUP_DISTRICT_CENTER },
+    { key: 'districtSt', title: '\u20033.1 项目负责人意见', opinionField: 'districtSt', pointsField: 'districtStPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_ST },
+    { key: 'districtNd', title: '\u20033.2 部门主任意见', opinionField: 'districtNd', pointsField: 'districtNdPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_ND },
+    { key: 'districtRd', title: '\u20033.3 分管领导意见', opinionField: 'districtRd', pointsField: 'districtRdPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_RD },
+    { key: 'districtTh', title: '\u20033.4 公服处意见', opinionField: 'districtTh', pointsField: 'districtThPoints', rejectTypeValue: 4, groupId: GROUP_DISTRICT_TH },
+    //{ key: 'districtFi', title: '3.5 公服处-分管领导意见', opinionField: 'districtFi', pointsField: 'districtFiPoints', rejectTypeValue: -1, groupId: GROUP_DISTRICT_FI },
+  ]
+  if (props.currentForm.level === LEVEL_NATIONAL)
+    result.push({ key: 'province', title: '5. 省文化和旅游厅意见', opinionField: 'province', pointsField: 'provincePoints', rejectTypeValue: 5, groupId: GROUP_PROVINCE });
+  return result;
+});
 
 const columns = [
   { title: '审核环节', dataIndex: 'title', width: 220 },
@@ -96,7 +101,7 @@ const columns = [
 
 const tableData = computed(() => {
   const reasons = props.currentForm.reasons || {};
-  return sections.map(sec => ({
+  return sections.value.map(sec => ({
     key: sec.key,
     title: sec.title,
     opinionField: sec.opinionField,

+ 297 - 0
src/pages/collect/assessment/evaluation-form-list-block.vue

@@ -0,0 +1,297 @@
+<template>
+  <CommonListBlock
+    ref="list2Ref"
+    :show-total="true"
+    :row-count="1"
+    :row-type="5"
+    :page-size="10"
+    :drop-down-names="[
+      {
+        options: selfAssessmentProgressOptions,
+        label: '状态',
+        defaultSelectedValue: lastSelfAssessmentProgress,
+      },
+      {
+        options: [
+          { id: 0, name: '全部审核' },
+          { id: 1, name: '审核通过' },
+          { id: 2, name: '审核退回' },
+        ],
+        label: '审核状态',
+        defaultSelectedValue: 0,
+      }
+    ]"
+    :load="(page: number, pageSize: number, _tag: number, searchText: string, drop: number[]) => loadSelfAssessmentAdminList(page, pageSize, searchText, drop)"
+    :show-detail="handleReviewSelfAssessment"
+  >
+    <template #headLeft>
+      <div class="flex flex-row items-center gap-2">
+        <a-button type="primary" @click="openDownloadModal">打包下载</a-button>
+        <a-button @click="openExportExcelModal">导出Excel</a-button>
+        <span>{{ progressHint }}</span>
+      </div>
+    </template>
+    <template #itemRight="{ item }">
+      <a-popover v-if="item.rejectType && item.rejectType > 0" title="退回原因" trigger="hover">
+        <template #content>
+          <div style="max-width: 300px">{{ item.rejectReason || '无' }}</div>
+        </template>
+        <span class="mr-3 text-sm" style="color: #f5222d; cursor: pointer;">
+          <ExclamationCircleOutlined /> 已退回
+        </span>
+      </a-popover>
+      <span 
+        v-else 
+        class="mr-3 text-sm text-gray-600"
+      >
+        {{ selfAssessmentProgressLabel(item.progress) }}
+      </span>
+      <a-button type="link" @click.stop="router.push({ name: 'CollectEvaluationForm', query: { id: item.checkId ?? item.id, userId: item.userId } })">编辑</a-button>
+      
+      <div v-if="item.checkId && canReviewButPrevNotCompleted(item.progress)" class="flex flex-row items-center mr-3 text-sm">
+        <ExclamationCircleOutlined /> 上一阶段未审核完成
+      </div>
+      <a-button
+        type="primary"
+        v-else-if="item.checkId && canReview(item.progress)"
+        @click.stop="handleReviewSelfAssessment(item)"
+      >
+        审核
+      </a-button>
+      <a-button
+        type="primary"
+        v-else-if="item.agreementId && isCurrentLevelReviewed(item.progress)"
+        disabled
+      >
+        已审核
+      </a-button>
+    </template>
+  </CommonListBlock>
+
+  <a-modal
+    v-model:open="showDownloadModal"
+    title="打包下载自查表"
+    @ok="handleDownloadZip"
+    :confirmLoading="downloading"
+    okText="下载"
+    cancelText="取消"
+  >
+    <a-form layout="vertical">
+      <a-form-item label="年份">
+        <a-input-number v-model:value="downloadYear" :min="2020" :max="2030" style="width: 100%" />
+      </a-form-item>
+      <a-form-item label="状态">
+        <a-select v-model:value="downloadProgress" style="width: 100%">
+          <a-select-option v-for="opt in downloadProgressOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
+        </a-select>
+      </a-form-item>
+    </a-form>
+  </a-modal>
+
+  <a-modal
+    v-model:open="showExportExcelModal"
+    title="导出自查表汇总Excel"
+    @ok="handleExportExcel"
+    :confirmLoading="exporting"
+    okText="导出"
+    cancelText="取消"
+  >
+    <a-form layout="vertical">
+      <a-form-item label="年份">
+        <a-input-number v-model:value="exportYear" :min="2020" :max="2030" style="width: 100%" />
+      </a-form-item>
+      <a-form-item label="状态">
+        <a-select v-model:value="exportProgress" style="width: 100%">
+          <a-select-option v-for="opt in downloadProgressOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="等级">
+        <span v-if="props.level != null">{{ selfAssessmentLevelOptions.find((opt) => opt.id === props.level)?.name }}</span>
+        <a-select v-else v-model:value="exportLevel" style="width: 100%">
+          <a-select-option v-for="opt in selfAssessmentLevelOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
+        </a-select>
+      </a-form-item>
+    </a-form>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useMemorizeVar } from '@/composeables/useMemorizeVar';
+import { useReview } from './composeables/Review';
+import { message } from 'ant-design-vue';
+import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
+import  AssessmentContentApi, { SelfAssessmentDetail } from '@/api/collect/AssessmentContent';
+import CommonListBlock, { type DropdownCommonItem } from '@/components/content/CommonListBlock.vue';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts';
+
+const router = useRouter();
+const route = useRoute();
+
+const props = defineProps<{
+  level?: number;
+}>();
+
+const list2Ref = ref<InstanceType<typeof CommonListBlock>>();
+
+const showDownloadModal = ref(false);
+const downloading = ref(false);
+const downloadYear = ref(new Date().getFullYear() - 1);
+const downloadProgress = ref(-100);
+
+const showExportExcelModal = ref(false);
+const exporting = ref(false);
+const exportYear = ref(new Date().getFullYear() - 1);
+const exportLevel = ref(props.level ?? 0);
+const exportProgress = ref(-100);
+
+const { variable: lastSelfAssessmentProgress } = useMemorizeVar('adminSelfAssessmentProgress', -100);
+const { variable: lastSelfAssessmentLevel } = useMemorizeVar('adminSelfAssessmentLevel', 0);
+
+const { canReview, canReviewButPrevNotCompleted, isCurrentLevelReviewed, progressHint } = useReview();
+
+const selfAssessmentLevelOptions: DropdownCommonItem[] = [
+  { id: 0, name: '全部等级' },
+  { id: LEVEL_NATIONAL, name: '国家级' },
+  { id: LEVEL_PROVINCE, name: '省级' },
+  { id: LEVEL_DISTRICT, name: '市级' },
+];
+
+const selfAssessmentProgressOptions: DropdownCommonItem[] = [
+  { id: -100, name: '全部状态' },
+  { id: -1, name: '未提交' },
+  { id: 0, name: '草稿' },
+  { id: 1, name: '已提交审核' },
+  { id: 2, name: '项目保护单位审核完成' },
+  { id: 3, name: '县(区)文旅部门审核完成' },
+  { id: 4, name: '设区市文旅部门/省非遗中心审核完成' },
+  { id: 5, name: '省文化和旅游厅审核完成' },
+];
+
+const downloadProgressOptions: DropdownCommonItem[] = [
+  { id: -100, name: '全部状态' },
+  { id: -1, name: '未提交' },
+  { id: 0, name: '草稿' },
+  { id: 1, name: '已提交审核' },
+  { id: 2, name: '项目保护单位审核完成' },
+  { id: 3, name: '县(区)文旅部门审核完成' },
+  { id: 4, name: '设区市文旅部门/省非遗中心审核完成' },
+  { id: 5, name: '省文化和旅游厅审核完成' },
+];
+
+function selfAssessmentProgressLabel(progress: number | null | undefined) {
+  if (progress === null || progress === undefined)
+    return '未填写';
+  const hit = selfAssessmentProgressOptions.find((o) => o.id === progress);
+  return hit?.name ?? `进度 ${progress}`;
+}
+
+async function loadSelfAssessmentAdminList(page: number, pageSize: number, searchText: string, dropDownValues: number[]) {
+  const pv = dropDownValues?.[0];
+  const lv = dropDownValues?.[1];
+  const st = dropDownValues?.[2];
+  lastSelfAssessmentProgress.value = pv ?? -100;
+  lastSelfAssessmentLevel.value = lv ?? 0;
+  const progress = pv != null && pv > -50 ? pv : undefined;
+  const level = lv != null && lv > 0 ? lv : undefined;
+  const list = await AssessmentContentApi.getInheritorList({
+    year: new Date().getFullYear(),
+    page,
+    pageSize,
+    keywords: searchText?.trim() || undefined,
+    progress,
+    level,
+    logStatus: st > 0 ? st : undefined,
+  });
+  return {
+    page,
+    total: list.total,
+    data: list.data.map((row) => ({
+      ...row,
+      id: row.id,
+      userId: row.userId,
+      title: row.title ?? '?',
+      desc: [row.mobile, row.unit, row.ichTitle].filter(Boolean).join(' · ') || '—',
+    })),
+  };
+}
+
+function handleReviewSelfAssessment(item: SelfAssessmentDetail) {
+  const cid = item.checkId;
+  const uid = item.userId;
+  if (!cid || !uid) {
+    message.warning('缺少自查表 ID 或传承人用户 ID');
+    return;
+  }
+  router.push({
+    name: 'CollectEvaluationFormReview',
+    query: {
+      id: String(cid),
+      userId: String(uid),
+      ...(item.progress != null && item.progress !== undefined ? { progress: String(item.progress) } : {}),
+    },
+  });
+}
+
+async function handleDownloadZip() {
+  downloading.value = true;
+  try {
+    const progress = downloadProgress.value > -50 ? downloadProgress.value : undefined;
+    await AssessmentContentApi.downloadCheckZip({
+      year: downloadYear.value,
+      level: props.level ?? 0,
+      progress,
+    });
+    showDownloadModal.value = false;
+    message.success('下载成功');
+  } catch (e: any) {
+    message.error(e?.message || '下载失败');
+  } finally {
+    downloading.value = false;
+  }
+}
+
+function openDownloadModal() {
+  downloadProgress.value = lastSelfAssessmentProgress.value;
+  showDownloadModal.value = true;
+}
+
+function openExportExcelModal() {
+  exportProgress.value = lastSelfAssessmentProgress.value;
+  exportLevel.value = lastSelfAssessmentLevel.value;
+  showExportExcelModal.value = true;
+}
+
+async function handleExportExcel() {
+  exporting.value = true;
+  try {
+    const progress = exportProgress.value > -50 ? exportProgress.value : undefined;
+    const level = exportLevel.value > 0 ? exportLevel.value : undefined;
+    await AssessmentContentApi.exportCheckExcel({
+      year: exportYear.value,
+      progress,
+      level,
+    });
+    showExportExcelModal.value = false;
+    message.success('导出成功');
+  } catch (e: any) {
+    message.error(e?.message || '导出失败');
+  } finally {
+    exporting.value = false;
+  }
+}
+
+watch(() => props.level, () => {
+  list2Ref.value?.reload();
+  exportLevel.value = props.level ?? 0;
+}, { immediate: true });
+
+defineExpose({
+  changeProgressFilter: (progress: number) => {
+    lastSelfAssessmentProgress.value = progress;
+    list2Ref.value?.setDropDownValue(progress, 0);
+  },
+  reload: () => list2Ref.value?.reload(),
+});
+</script>

+ 112 - 0
src/pages/collect/assessment/evaluation-form-list-summary.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="mb-4 p-4 bg-gray-50 rounded border border-gray-200 flex flex-row flex-wrap">
+    <div class="w-full lg:w-1/2 flex flex-row flex-wrap">
+      <div 
+        v-for="s in checkProgressStats" :key="s.key" 
+        class="flex-1 flex flex-col p-2 items-center cursor-pointer" 
+        :style="{width: checkProgressStatsSpan + '%'}"
+        @click="handleClickProgress(s.key)"
+      >
+        <span class="text-2xl bold">{{ s.count }}</span>
+        <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsis">{{ s.label }}</span>
+      </div>
+      <div class="p-2" :style="{width: checkProgressStatsSpan + '%'}">
+        <v-chart :option="checkPieChartOption" autoresize />
+      </div>
+    </div>
+    <div class="w-full lg:w-1/2 flex flex-row flex-wrap border-l border-gray-200">
+      <div v-for="s in checkRejectStats" :key="s.key" class="flex-1 flex flex-col p-2 items-center" :style="{width: checkRejectStatsSpan + '%'}">
+        <span class="text-2xl bold">{{ s.count }}</span>
+        <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsi">{{ s.label }}</span>
+      </div>
+      <div class="p-2" :style="{width: checkRejectStatsSpan + '%'}">
+        <v-chart :option="checkBarChartOption" autoresize />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue';
+import { use } from "echarts/core";
+import { CanvasRenderer } from "echarts/renderers";
+import { PieChart, BarChart } from "echarts/charts";
+import {
+  TooltipComponent,
+  GridComponent,
+} from "echarts/components";
+use([CanvasRenderer, PieChart, BarChart, TooltipComponent, GridComponent]);
+import VChart from "vue-echarts";
+import  AssessmentContentApi from '@/api/collect/AssessmentContent';
+
+const props = defineProps<{
+  level?: number;
+}>();
+const emit = defineEmits(['changeProgressFilter']);
+
+const dataStats = ref<Awaited<ReturnType<typeof AssessmentContentApi.getDataCount>>|null>(null);
+
+function handleClickProgress(key: number) {
+  emit('changeProgressFilter', key);
+}
+
+const checkProgressStats = computed(() => {
+  if (!dataStats.value?.check) return [];
+  return dataStats.value.check.map((d) => ({ key: d.key, label: d.title, count: d.value, isReject: false }));
+});
+const checkProgressStatsSpan = computed(() => {
+  const len = checkProgressStats.value.length;
+  return len > 1 ? 100 / (len - 1) : 100;
+});
+const checkRejectStats = computed(() => {
+  if (!dataStats.value?.checkReject) return [];
+  return dataStats.value.checkReject.map((d) => ({ key: 'r' + d.key, label: d.title, count: d.value, isReject: true }));
+});
+const checkRejectStatsSpan = computed(() => {
+  const len = checkRejectStats.value.length;
+  return len > 1 ? 100 / (len - 1) : 100;
+});
+
+const checkPieChartOption = computed(() => {
+  const data = (dataStats.value?.check || []).map((s: any) => ({ name: s.title, value: s.value }));
+  return {
+    tooltip: { trigger: 'item' },
+    series: [{
+      type: 'pie',
+      radius: ['40%', '70%'],
+      center: ['50%', '50%'],
+      data,
+      label: { show: false },
+      emphasis: { label: { show: true } },
+    }],
+  };
+});
+
+const checkBarChartOption = computed(() => {
+  const data = (dataStats.value?.checkReject || []).map((s: any) => s.value);
+  const titles = (dataStats.value?.checkReject || []).map((s: any) => s.title);
+  const max = Math.max(...data, 1);
+  return {
+    tooltip: { trigger: 'axis' },
+    grid: { left: 0, right: 0, top: 8, bottom: 0, containLabel: true },
+    xAxis: { type: 'category', data: titles, axisLabel: { show: false }, axisTick: { show: false } },
+    yAxis: { type: 'value', axisLabel: { show: false }, splitLine: { show: false }, max },
+    series: [
+      { type: 'bar', data: data.map(v => max - v), barWidth: 12, itemStyle: { color: '#f0f0f0', borderRadius: 2 }, stack: 'total', silent: true, z: 1 },
+      { type: 'bar', data, barWidth: 12, itemStyle: { color: '#1890ff', borderRadius: 2 }, stack: 'total', z: 2 },
+    ],
+  };
+});
+
+
+async function loadStats() {
+  dataStats.value = await AssessmentContentApi.getDataCount(props.level);
+}
+
+watch(() => props.level, () => {
+  loadStats();
+})
+onMounted(() => {
+  loadStats();
+})
+</script>

+ 31 - 373
src/pages/collect/assessment/evaluation-form-list.vue

@@ -1,101 +1,30 @@
 <template>
   <a-tabs v-model:activeKey="activeKey" centered type="card">
     <a-tab-pane key="1" tab="传承人列表">
-      <div class="mb-4 p-4 bg-gray-50 rounded border border-gray-200 flex flex-row flex-wrap">
-        <div class="w-full lg:w-1/2 flex flex-row flex-wrap">
-          <div v-for="s in checkProgressStats" :key="s.key" class="flex-1 flex flex-col p-2 items-center" :style="{width: checkProgressStatsSpan + '%'}">
-            <span class="text-2xl bold">{{ s.count }}</span>
-            <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsis">{{ s.label }}</span>
-          </div>
-          <div class="p-2" :style="{width: checkProgressStatsSpan + '%'}">
-            <v-chart :option="checkPieChartOption" autoresize />
-          </div>
-        </div>
-        <div class="w-full lg:w-1/2 flex flex-row flex-wrap border-l border-gray-200">
-          <div v-for="s in checkRejectStats" :key="s.key" class="flex-1 flex flex-col p-2 items-center" :style="{width: checkRejectStatsSpan + '%'}">
-            <span class="text-2xl bold">{{ s.count }}</span>
-            <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsi">{{ s.label }}</span>
-          </div>
-          <div class="p-2" :style="{width: checkRejectStatsSpan + '%'}">
-            <v-chart :option="checkBarChartOption" autoresize />
-          </div>
-        </div>
-      </div>
-      <CommonListBlock
-        ref="listRef"
-        :show-total="true"
-        :row-count="1"
-        :row-type="5"
-        :page-size="10"
-        :drop-down-names="[
-          {
-            options: selfAssessmentProgressOptions,
-            label: '状态',
-            defaultSelectedValue: lastSelfAssessmentProgress,
-          },
-          {
-            options: selfAssessmentLevelOptions,
-            label: '等级',
-            defaultSelectedValue: lastSelfAssessmentLevel,
-          },
-          {
-            options: [
-              { id: 0, name: '全部审核' },
-              { id: 1, name: '审核通过' },
-              { id: 2, name: '审核退回' },
-            ],
-            label: '审核状态',
-            defaultSelectedValue: 0,
-          }
-        ]"
-        :load="(page: number, pageSize: number, _tag: number, searchText: string, drop: number[]) => loadSelfAssessmentAdminList(page, pageSize, searchText, drop)"
-        :show-detail="handleReviewSelfAssessment"
-      >
-        <template #headLeft>
-          <div class="flex flex-row items-center gap-2">
-            <a-button type="primary" @click="openDownloadModal">打包下载</a-button>
-            <a-button @click="openExportExcelModal">导出Excel</a-button>
-            <span>{{ progressHint }}</span>
-          </div>
-        </template>
-        <template #itemRight="{ item }">
-          <a-popover v-if="item.rejectType && item.rejectType > 0" title="退回原因" trigger="hover">
-            <template #content>
-              <div style="max-width: 300px">{{ item.rejectReason || '无' }}</div>
-            </template>
-            <span class="mr-3 text-sm" style="color: #f5222d; cursor: pointer;">
-              <ExclamationCircleOutlined /> 已退回
-            </span>
-          </a-popover>
-          <span 
-            v-else 
-            class="mr-3 text-sm text-gray-600"
-          >
-            {{ selfAssessmentProgressLabel(item.progress) }}
-          </span>
-          <a-button type="link" @click.stop="router.push({ name: 'CollectEvaluationForm', query: { id: item.checkId ?? item.id, userId: item.userId } })">编辑</a-button>
-          
-          <div v-if="item.checkId && canReviewButPrevNotCompleted(item.progress)" class="flex flex-row items-center mr-3 text-sm">
-            <ExclamationCircleOutlined /> 上一阶段未审核完成
-          </div>
-          <a-button
-            type="primary"
-            v-else-if="item.checkId && canReview(item.progress)"
-            @click.stop="handleReviewSelfAssessment(item)"
-          >
-            审核
-          </a-button>
-          <a-button
-            type="primary"
-            v-else-if="item.agreementId && isCurrentLevelReviewed(item.progress)"
-            disabled
-          >
-            已审核
-          </a-button>
-        </template>
-      </CommonListBlock>
+      <template v-if="activeKey==='1'">
+        <EvaluationFormListSummary @changeProgressFilter="subListRef?.changeProgressFilter" />
+        <EvaluationFormListBlock ref="subListRef" />
+      </template>
     </a-tab-pane>
-    <a-tab-pane key="2" tab="审核记录">
+    <a-tab-pane key="2" tab="国家级传承人">
+      <template v-if="activeKey==='2'">
+        <EvaluationFormListSummary :level="LEVEL_NATIONAL" @changeProgressFilter="subListRef?.changeProgressFilter" />
+        <EvaluationFormListBlock ref="subListRef" :level="LEVEL_NATIONAL" />
+      </template>
+    </a-tab-pane>
+    <a-tab-pane key="3" tab="省级传承人">
+      <template v-if="activeKey==='3'">
+        <EvaluationFormListSummary :level="LEVEL_PROVINCE" @changeProgressFilter="subListRef?.changeProgressFilter" />
+        <EvaluationFormListBlock ref="subListRef" :level="LEVEL_PROVINCE" />
+      </template> 
+    </a-tab-pane>
+    <a-tab-pane key="4" tab="市级传承人">
+      <template v-if="activeKey==='4'">
+        <EvaluationFormListSummary :level="LEVEL_DISTRICT" @changeProgressFilter="subListRef?.changeProgressFilter" />
+        <EvaluationFormListBlock ref="subListRef" :level="LEVEL_DISTRICT" />
+      </template> 
+    </a-tab-pane>
+    <a-tab-pane key="10" tab="审核记录">
       <CommonListBlock
         ref="listRef"
         :show-total="true"
@@ -124,187 +53,28 @@
       </CommonListBlock>
     </a-tab-pane>
   </a-tabs>
-
-  <a-modal
-    v-model:open="showDownloadModal"
-    title="打包下载自查表"
-    @ok="handleDownloadZip"
-    :confirmLoading="downloading"
-    okText="下载"
-    cancelText="取消"
-  >
-    <a-form layout="vertical">
-      <a-form-item label="年份">
-        <a-input-number v-model:value="downloadYear" :min="2020" :max="2030" style="width: 100%" />
-      </a-form-item>
-      <a-form-item label="状态">
-        <a-select v-model:value="downloadProgress" style="width: 100%">
-          <a-select-option v-for="opt in downloadProgressOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
-        </a-select>
-      </a-form-item>
-    </a-form>
-  </a-modal>
-
-  <a-modal
-    v-model:open="showExportExcelModal"
-    title="导出自查表汇总Excel"
-    @ok="handleExportExcel"
-    :confirmLoading="exporting"
-    okText="导出"
-    cancelText="取消"
-  >
-    <a-form layout="vertical">
-      <a-form-item label="年份">
-        <a-input-number v-model:value="exportYear" :min="2020" :max="2030" style="width: 100%" />
-      </a-form-item>
-      <a-form-item label="等级">
-        <a-select v-model:value="exportLevel" style="width: 100%">
-          <a-select-option v-for="opt in selfAssessmentLevelOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
-        </a-select>
-      </a-form-item>
-      <a-form-item label="状态">
-        <a-select v-model:value="exportProgress" style="width: 100%">
-          <a-select-option v-for="opt in downloadProgressOptions" :key="opt.id" :value="opt.id">{{ opt.name }}</a-select-option>
-        </a-select>
-      </a-form-item>
-    </a-form>
-  </a-modal>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, ref, watch } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { ref, watch, type Ref } from 'vue';
+import { useRoute } from 'vue-router';
 import { useMemorizeVar } from '@/composeables/useMemorizeVar';
-import { useReview } from './composeables/Review';
-import { message } from 'ant-design-vue';
-import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
-import { use } from "echarts/core";
-import { CanvasRenderer } from "echarts/renderers";
-import { PieChart, BarChart } from "echarts/charts";
-import {
-  TooltipComponent,
-  GridComponent,
-} from "echarts/components";
-use([CanvasRenderer, PieChart, BarChart, TooltipComponent, GridComponent]);
-import VChart from "vue-echarts";
 import CommonListBlock, { type DropdownCommonItem } from '@/components/content/CommonListBlock.vue';
-import  AssessmentContentApi, { SelfAssessmentDetail } from '@/api/collect/AssessmentContent';
+import AssessmentContentApi from '@/api/collect/AssessmentContent';
+import EvaluationFormListSummary from './evaluation-form-list-summary.vue';
+import EvaluationFormListBlock from './evaluation-form-list-block.vue';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
 
-const router = useRouter();
 const route = useRoute();
 
 const activeKey = ref('1');
-const listRef = ref<any>(null);
-
-const showDownloadModal = ref(false);
-const downloading = ref(false);
-const downloadYear = ref(new Date().getFullYear() - 1);
-const downloadProgress = ref(-100);
-
-const showExportExcelModal = ref(false);
-const exporting = ref(false);
-const exportYear = ref(new Date().getFullYear() - 1);
-const exportLevel = ref(0);
-const exportProgress = ref(-100);
-
-const dataStats = ref<Awaited<ReturnType<typeof AssessmentContentApi.getDataCount>>|null>(null);
-
-const checkProgressLabels: Record<string, string> = {
-  '-1': '未提交',
-  '0': '草稿',
-  '1': '已提交审核',
-  '2': '单位审核完成',
-  '3': '县级审核完成',
-  '4': '市级审核完成',
-  '5': '省级审核完成',
-};
-
-const checkRejectLabels: Record<string, string> = {
-  '1': '自评阶段退回',
-  '2': '项目保护单位退回',
-  '3': '县(区)退回',
-  '4': '设区市/省非遗中心退回',
-  '5': '省文旅厅退回',
-};
-
-const checkProgressStats = computed(() => {
-  if (!dataStats.value?.check) return [];
-  return dataStats.value.check.map((d) => ({ key: d.key, label: d.title, count: d.value, isReject: false }));
-});
-const checkProgressStatsSpan = computed(() => {
-  const len = checkProgressStats.value.length;
-  return len > 1 ? 100 / (len - 1) : 100;
-});
-const checkRejectStats = computed(() => {
-  if (!dataStats.value?.checkReject) return [];
-  return dataStats.value.checkReject.map((d) => ({ key: 'r' + d.key, label: d.title, count: d.value, isReject: true }));
-});
-const checkRejectStatsSpan = computed(() => {
-  const len = checkRejectStats.value.length;
-  return len > 1 ? 100 / (len - 1) : 100;
-});
-
-const checkPieChartOption = computed(() => {
-  const data = (dataStats.value?.check || []).map((s: any) => ({ name: s.title, value: s.value }));
-  return {
-    tooltip: { trigger: 'item' },
-    series: [{
-      type: 'pie',
-      radius: ['40%', '70%'],
-      center: ['50%', '50%'],
-      data,
-      label: { show: false },
-      emphasis: { label: { show: true } },
-    }],
-  };
-});
-
-const checkBarChartOption = computed(() => {
-  const data = (dataStats.value?.checkReject || []).map((s: any) => s.value);
-  const titles = (dataStats.value?.checkReject || []).map((s: any) => s.title);
-  const max = Math.max(...data, 1);
-  return {
-    tooltip: { trigger: 'axis' },
-    grid: { left: 0, right: 0, top: 8, bottom: 0, containLabel: true },
-    xAxis: { type: 'category', data: titles, axisLabel: { show: false }, axisTick: { show: false } },
-    yAxis: { type: 'value', axisLabel: { show: false }, splitLine: { show: false }, max },
-    series: [
-      { type: 'bar', data: data.map(v => max - v), barWidth: 12, itemStyle: { color: '#f0f0f0', borderRadius: 2 }, stack: 'total', silent: true, z: 1 },
-      { type: 'bar', data, barWidth: 12, itemStyle: { color: '#1890ff', borderRadius: 2 }, stack: 'total', z: 2 },
-    ],
-  };
-});
-
+const listRef = ref<InstanceType<typeof CommonListBlock>>();
+const subListRef = ref<InstanceType<typeof EvaluationFormListBlock>>();
 
-onMounted(async () => {
-  dataStats.value = await AssessmentContentApi.getDataCount();
-});
-
-const { variable: lastSelfAssessmentProgress } = useMemorizeVar('adminSelfAssessmentProgress', -100);
 const { variable: lastSelfAssessmentLevel } = useMemorizeVar('adminSelfAssessmentLevel', 0);
 const { variable: lastCheckLogStatus } = useMemorizeVar('adminCheckLogStatus', 0);
 const { variable: lastCheckLogReviewType } = useMemorizeVar('adminCheckLogReviewType', 0);
 
-const { canReview, canReviewButPrevNotCompleted, isCurrentLevelReviewed, progressHint } = useReview();
-
-const selfAssessmentLevelOptions: DropdownCommonItem[] = [
-  { id: 0, name: '全部等级' },
-  { id: 23, name: '国家级' },
-  { id: 24, name: '省级' },
-  { id: 25, name: '市级' },
-];
-
-const selfAssessmentProgressOptions: DropdownCommonItem[] = [
-  { id: -100, name: '全部状态' },
-  { id: -1, name: '未提交' },
-  { id: 0, name: '草稿' },
-  { id: 1, name: '已提交审核' },
-  { id: 2, name: '项目保护单位审核完成' },
-  { id: 3, name: '县(区)文旅部门审核完成' },
-  { id: 4, name: '设区市文旅部门/省非遗中心审核完成' },
-  { id: 5, name: '省文化和旅游厅审核完成' },
-];
-
 const checkLogStatusOptions: DropdownCommonItem[] = [
   { id: 0, name: '全部状态' },
   { id: 1, name: '通过' },
@@ -320,54 +90,6 @@ const checkLogReviewTypeOptions: DropdownCommonItem[] = [
   { id: 5, name: '省文化和旅游厅' },
 ];
 
-const downloadProgressOptions: DropdownCommonItem[] = [
-  { id: -100, name: '全部状态' },
-  { id: -1, name: '未提交' },
-  { id: 0, name: '草稿' },
-  { id: 1, name: '已提交审核' },
-  { id: 2, name: '项目保护单位审核完成' },
-  { id: 3, name: '县(区)文旅部门审核完成' },
-  { id: 4, name: '设区市文旅部门/省非遗中心审核完成' },
-  { id: 5, name: '省文化和旅游厅审核完成' },
-];
-
-function selfAssessmentProgressLabel(progress: number | null | undefined) {
-  if (progress === null || progress === undefined)
-    return '未填写';
-  const hit = selfAssessmentProgressOptions.find((o) => o.id === progress);
-  return hit?.name ?? `进度 ${progress}`;
-}
-
-async function loadSelfAssessmentAdminList(page: number, pageSize: number, searchText: string, dropDownValues: number[]) {
-  const pv = dropDownValues?.[0];
-  const lv = dropDownValues?.[1];
-  const st = dropDownValues?.[2];
-  lastSelfAssessmentProgress.value = pv ?? -100;
-  lastSelfAssessmentLevel.value = lv ?? 0;
-  const progress = pv != null && pv > -50 ? pv : undefined;
-  const level = lv != null && lv > 0 ? lv : undefined;
-  const list = await AssessmentContentApi.getInheritorList({
-    year: new Date().getFullYear(),
-    page,
-    pageSize,
-    keywords: searchText?.trim() || undefined,
-    progress,
-    level,
-    logStatus: st > 0 ? st : undefined,
-  });
-  return {
-    page,
-    total: list.total,
-    data: list.data.map((row) => ({
-      ...row,
-      id: row.id,
-      userId: row.userId,
-      title: row.title ?? '?',
-      desc: [row.mobile, row.unit, row.ichTitle].filter(Boolean).join(' · ') || '—',
-    })),
-  };
-}
-
 async function loadCheckLogList(page: number, pageSize: number, searchText: string, dropDownValues: number[]) {
   const sv = dropDownValues?.[0];
   const rv = dropDownValues?.[1];
@@ -400,70 +122,6 @@ async function loadCheckLogList(page: number, pageSize: number, searchText: stri
   };
 }
 
-function handleReviewSelfAssessment(item: SelfAssessmentDetail) {
-  const cid = item.checkId;
-  const uid = item.userId;
-  if (!cid || !uid) {
-    message.warning('缺少自查表 ID 或传承人用户 ID');
-    return;
-  }
-  router.push({
-    name: 'CollectEvaluationFormReview',
-    query: {
-      id: String(cid),
-      userId: String(uid),
-      ...(item.progress != null && item.progress !== undefined ? { progress: String(item.progress) } : {}),
-    },
-  });
-}
-
-async function handleDownloadZip() {
-  downloading.value = true;
-  try {
-    const progress = downloadProgress.value > -50 ? downloadProgress.value : undefined;
-    await AssessmentContentApi.downloadCheckZip({
-      year: downloadYear.value,
-      progress,
-    });
-    showDownloadModal.value = false;
-    message.success('下载成功');
-  } catch (e: any) {
-    message.error(e?.message || '下载失败');
-  } finally {
-    downloading.value = false;
-  }
-}
-
-function openDownloadModal() {
-  downloadProgress.value = lastSelfAssessmentProgress.value;
-  showDownloadModal.value = true;
-}
-
-function openExportExcelModal() {
-  exportProgress.value = lastSelfAssessmentProgress.value;
-  exportLevel.value = lastSelfAssessmentLevel.value;
-  showExportExcelModal.value = true;
-}
-
-async function handleExportExcel() {
-  exporting.value = true;
-  try {
-    const progress = exportProgress.value > -50 ? exportProgress.value : undefined;
-    const level = exportLevel.value > 0 ? exportLevel.value : undefined;
-    await AssessmentContentApi.exportCheckExcel({
-      year: exportYear.value,
-      progress,
-      level,
-    });
-    showExportExcelModal.value = false;
-    message.success('导出成功');
-  } catch (e: any) {
-    message.error(e?.message || '导出失败');
-  } finally {
-    exporting.value = false;
-  }
-}
-
 watch(route, () => {
   listRef.value?.reload();
 })

+ 14 - 13
src/pages/collect/assessment/evaluation-form-review.vue

@@ -162,26 +162,26 @@
                         保存意见
                       </a-button>
                     </div>
-                    <div class="flex flex-col flex-1">
+                    <div v-if="reviewProgressInfo?.canReject" class="flex flex-col flex-1">
                       <a-button
                         block
-                        type="primary"
-                        :loading="submitLoading"
+                        danger
+                        :loading="rejectLoading"
                         :disabled="!canSubmitReview"
-                        @click="submitReview"
+                        @click="submitReject"
                       >
-                        {{ isNoProgressChange ? '提交意见' : '通过审核' }}
+                        不合格回退
                       </a-button>
                     </div>
-                    <div v-if="reviewProgressInfo?.canReject" class="flex flex-col flex-1">
+                    <div class="flex flex-col flex-1">
                       <a-button
                         block
-                        danger
-                        :loading="rejectLoading"
+                        type="primary"
+                        :loading="submitLoading"
                         :disabled="!canSubmitReview"
-                        @click="submitReject"
+                        @click="submitReview"
                       >
-                        回退
+                        通过审核
                       </a-button>
                     </div>
                   </template>
@@ -214,6 +214,7 @@ import { useMemorizeVar } from '@/composeables/useMemorizeVar';
 import { GROUP_TO_REVIEW_PROGRESS, GROUP_DISTRICT_ST, GROUP_DISTRICT_ND, GROUP_DISTRICT_RD, GROUP_DISTRICT_TH, GROUP_DISTRICT_FI, PROGRESS_SUBMIT_LABELS, GROUP_FIELD_MAP, type GroupFieldDef } from './composeables/GroupData.ts';
 import { isInMiniProgram } from '@/composeables/MiniProgramIng.ts';
 import { useReview } from './composeables/Review.ts';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
 
 function formatErr(e: unknown): string {
   if (e instanceof RequestApiError)
@@ -297,9 +298,9 @@ const alreadySubmitted = computed(() => {
 });
 
 function levelTitleFromForm(f: SelfAssessmentDetail) {
-  if (f.level === 23) return '国家级';
-  if (f.level === 24) return '省级';
-  if (f.level === 25) return '市级';
+  if (f.level === LEVEL_NATIONAL) return '国家级';
+  if (f.level === LEVEL_PROVINCE) return '省级';
+  if (f.level === LEVEL_DISTRICT) return '市级';
   return '国家级';
 }
 

+ 54 - 7
src/pages/collect/assessment/evaluation-form.vue

@@ -40,12 +40,23 @@
             <a-space direction="vertical" class="w-full" size="middle">
               <a-button type="primary" block :loading="submitLoading" @click="saveForm">保存评估表</a-button>
               <a-button v-if="!authStore.isAdmin && currentForm?.progress === 0" type="primary" block :loading="submitLoading" @click="submitForm">提交审核</a-button>
-              <a-button block :loading="submitLoading" @click="downloadForm">下载评估表 PDF</a-button>
+              <div class="flex flex-row gap-2">
+                <a-button block @click="previewForm">预览评估表 PDF</a-button>
+                <a-button block :loading="submitLoading" @click="downloadForm">下载评估表 PDF</a-button>
+              </div>
             </a-space>
           </div>
         </a-spin>
       </div>
     </section>
+    <a-modal
+      title="预览评估表 PDF"
+      v-model:visible="previewVisible"
+      width="80%"
+      :footer="null"
+    >
+      <iframe :src="previewUrl" class="w-full h-full" style="min-height: 60vh"></iframe>
+    </a-modal>
   </div>
 </template>
 
@@ -66,6 +77,8 @@ import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vu
 import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
 import { injectAppConfiguration } from '@/api/system/useAppConfiguration.ts';
 import { getFormErrorFieldsMessage } from '@/common/Form.ts';
+import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts.ts';
+
 
 function formatErr(e: unknown): string {
   if (e instanceof RequestApiError)
@@ -91,9 +104,9 @@ const blockRef = ref<InstanceType<typeof SelfAssessmentFormDisplay> | null>(null
 const submitLoading = ref(false);
 
 const levelTitle = computed(() => {
-  if (currentForm.value?.level === 23) return '国家级';
-  if (currentForm.value?.level === 24) return '省级';
-  if (currentForm.value?.level === 25) return '市级';
+  if (currentForm.value?.level === LEVEL_NATIONAL) return '国家级';
+  if (currentForm.value?.level === LEVEL_PROVINCE) return '省级';
+  if (currentForm.value?.level === LEVEL_DISTRICT) return '市级';
   return '国家级';
 });
 
@@ -162,7 +175,7 @@ async function saveForm() {
       return;
     }
   }
-  if (cf.progress > 0) {
+  /* if (cf.progress > 0) {
     const confirmed = await new Promise<boolean>((resolve) => {
       Modal.confirm({
         title: '提示',
@@ -175,10 +188,10 @@ async function saveForm() {
     });
     if (!confirmed)
       return;
-  }
+  } */
   cf.checkItems = currentFormCheckItems.value;
   try {
-    await AssessmentContentApi.saveSelfAssessment(cf as SelfAssessmentDetail, 0);
+    await AssessmentContentApi.saveSelfAssessment(cf as SelfAssessmentDetail/* , 0 */);
     message.success('保存评估表成功');
     await waitTimeOut(500);
     await loader.load();
@@ -231,6 +244,40 @@ async function submitForm() {
   submitLoading.value = false;
 }
 
+const previewVisible = ref(false);
+const previewUrl = ref('');
+
+function revokePreviewUrl() {
+  if (previewUrl.value) {
+    URL.revokeObjectURL(previewUrl.value);
+    previewUrl.value = '';
+  }
+}
+
+async function previewForm() {
+  if (!currentForm.value?.id) {
+    message.warning('请先保存评估表后再预览 PDF');
+    return;
+  }
+  try {
+    previewVisible.value = true;
+    revokePreviewUrl();
+    previewUrl.value = await AssessmentContentApi.previewSelfAssessmentPdf(currentForm.value.id);
+  } catch (error) {
+    Modal.error({
+      title: '预览评估表失败',
+      content: formatErr(error),
+    });
+  }
+}
+
+watch(previewVisible, (visible) => {
+  if (!visible) {
+    revokePreviewUrl();
+  }
+});
+
+
 async function downloadForm() {
   if (!currentForm.value?.id) {
     message.warning('请先保存评估表后再下载 PDF');

+ 10 - 7
src/pages/inheritor.vue

@@ -8,7 +8,7 @@
       <div class="content">
         <div class="title left-right">
           <span style="width:160px;"></span>
-          <h2>非遗数字化资源信息校对</h2>
+          <h2>非遗数字化资源信息</h2>
           <small class="text-secondary">技术支持:18649931391</small>
         </div>
        
@@ -161,16 +161,16 @@
               </a-badge>
             </template>
             <div class="flex flex-row mt-4 mb-4 gap-4">
-              <a-button class="flex-1 h-[240px]!" type="default" @click="router.push({ name: 'CollectEvaluationForm' })">
+              <a-button class="flex-1 h-35!" type="default" @click="router.push({ name: 'CollectEvaluationForm' })">
                 <div class="flex flex-col items-center">
-                  <img src="@/assets/images/icon/EvaluationForm.svg" class="w-[100px] h-[100px]" alt="自查评估表" />
-                  <span>自查评估表</span>
+                  <img src="@/assets/images/icon/EvaluationForm.svg" class="w-15 h-15" alt="自查评估表" />
+                  <span>{{ appConfiguration?.collectFormYear || '?' }}年 自查评估表</span>
                 </div>
               </a-button>
-              <a-button class="flex-1 h-[240px]!" type="default" @click="router.push({ name: 'CollectAgreementSign' })">
+              <a-button class="flex-1 h-35!" type="default" @click="router.push({ name: 'CollectAgreementSign' })">
                 <div class="flex flex-col items-center">
-                  <img src="@/assets/images/icon/AgreementSign.svg" class="w-[100px] h-[100px]" alt="传承协议签署" />
-                  <span>传承协议签署</span>
+                  <img src="@/assets/images/icon/AgreementSign.svg" class="w-15 h-15" alt="传承协议签署" />
+                  <span>{{ appConfiguration?.collectSignYear || '?' }}年传承协议签署</span>
                 </div>
               </a-button>
             </div>
@@ -201,6 +201,7 @@ import EmptyToRecord from '@/components/parts/EmptyToRecord.vue';
 import { useSimpleDataLoader } from '@/composeables/useSimpleDataLoader';
 import SimpleRichHtml from '@/components/content/SimpleRichHtml.vue';
 import ImageGrid from '@/components/content/ImageGrid.vue';
+import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
 
 const router = useRouter();
 const route = useRoute();
@@ -209,6 +210,8 @@ const planData = ref<PlanInfo[]>([]);
 const worksData = ref<InheritorWorkInfo[]>([]);
 const showInMessage = ref(false);
 
+const appConfiguration = injectAppConfiguration();
+
 watch(activeKey, (newValue) => {
   router.replace({ query: { tab: newValue } });
 });