瀏覽代碼

📦 列表修改与审核功能

快乐的梦鱼 7 小時之前
父節點
當前提交
f84ee1ae5b

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

@@ -59,6 +59,7 @@ export class SelfAssessmentListRow extends DataModel<SelfAssessmentListRow> {
       province: { clientSide: 'number', serverSide: 'number' },
       provincePoints: { clientSide: 'number', serverSide: 'number' },
       level: { clientSide: 'number', serverSide: 'string' },
+      progress: { clientSide: 'number', serverSide: 'number' },
     };
     this._convertKeyType = (key) => {
       if (key.endsWith('Text') || key.endsWith('_text')) {
@@ -93,6 +94,8 @@ export class SelfAssessmentListRow extends DataModel<SelfAssessmentListRow> {
   createtime = '' as string;
   updatetime = '' as string;
   selfText = '' as string;
+  /** -1=未提交,0=草稿,1=已自评,2~5 各级审核完成 */
+  progress = null as number|null;
 }
 
 /** 传承协议列表行 */
@@ -132,6 +135,157 @@ export class AgreementListRow extends DataModel<AgreementListRow> {
   deletetime = '' as string|null;
 }
 
+/** 保护单位账号:传承人传承协议分页列表行(ich/check/getUserAgreement) */
+export class UserAgreementListRow extends DataModel<UserAgreementListRow> {
+  constructor() {
+    super(UserAgreementListRow, '传承人传承协议列表项');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number' },
+      sitesId: { clientSide: 'number', serverSide: 'number' },
+      batch: { clientSide: 'number', serverSide: 'number' },
+      ichId: { clientSide: 'number', serverSide: 'number' },
+      agreementId: { clientSide: 'number', serverSide: 'number' },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      year: { clientSide: 'number', serverSide: 'number' },
+      apprentice: { clientSide: 'number', serverSide: 'number' },
+      activity: { clientSide: 'number', serverSide: 'number' },
+      course: { clientSide: 'number', serverSide: 'number' },
+      progress: { clientSide: 'number', serverSide: 'number' },
+      level: { clientSide: 'number', serverSide: 'string' },
+    };
+    this._convertKeyType = (key) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return { clientSide: 'string', serverSide: 'undefined' };
+      }
+      return undefined;
+    };
+  }
+
+  id = 0 as number;
+  title = '' as string;
+  associationId = '' as string;
+  sitesId = 0 as number;
+  level = null as number|string|null;
+  batch = 0 as number;
+  gender = '' as string;
+  basicMobile = '' as string|null;
+  basicUnit = '' as string|null;
+  ichId = 0 as number;
+  ichTitle = '' as string;
+  /** 已填写的传承协议记录 ID,未填写时为 null */
+  agreementId = null as number|null;
+  userId = null as number|null;
+  year = null as number|null;
+  partyA = '' as string|null;
+  partyB = '' as string|null;
+  apprentice = null as number|null;
+  activity = null as number|null;
+  course = null as number|null;
+  mobile = '' as string|null;
+  partyAMobile = '' as string|null;
+  partyAContact = '' as string|null;
+  idCard = '' as string|null;
+  health = '' as string|null;
+  ich = '' as string|null;
+  partyASign = '' as string|null;
+  partyBSign = '' as string|null;
+  /** -1=未提交,0=草稿,1=已自评,2=审核完成 */
+  progress = null as number|null;
+  createtime = '' as string|null;
+  updatetime = '' as string|null;
+  deletetime = '' as string|null;
+  batchText = '' as string;
+  ichSiteTypeText = '' as string;
+}
+
+/** 保护单位账号:传承人列表行(ich/check/getInheritorList) */
+export class InheritorCheckListRow extends DataModel<InheritorCheckListRow> {
+  constructor() {
+    super(InheritorCheckListRow, '传承人列表项');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number' },
+      sitesId: { clientSide: 'number', serverSide: 'number' },
+      batch: { clientSide: 'number', serverSide: 'number' },
+      ichId: { clientSide: 'number', serverSide: 'number' },
+      checkId: { clientSide: 'number', serverSide: 'number' },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      year: { clientSide: 'number', serverSide: 'number' },
+      weigh: { clientSide: 'number', serverSide: 'number' },
+      deductPoints: { clientSide: 'number', serverSide: 'number' },
+      points: { clientSide: 'number', serverSide: 'number' },
+      self: { clientSide: 'number', serverSide: 'number' },
+      ichUnit: { clientSide: 'number', serverSide: 'number' },
+      unitPoints: { clientSide: 'number', serverSide: 'number' },
+      county: { clientSide: 'number', serverSide: 'number' },
+      countyPoints: { clientSide: 'number', serverSide: 'number' },
+      district: { clientSide: 'number', serverSide: 'number' },
+      districtPoints: { clientSide: 'number', serverSide: 'number' },
+      province: { clientSide: 'number', serverSide: 'number' },
+      provincePoints: { clientSide: 'number', serverSide: 'number' },
+      progress: { clientSide: 'number', serverSide: 'number' },
+      level: { clientSide: 'number', serverSide: 'string' },
+    };
+    this._convertKeyType = (key) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return { clientSide: 'string', serverSide: 'undefined' };
+      }
+      return undefined;
+    };
+  }
+
+  id = 0 as number;
+  title = '' as string;
+  /** 关联项目 ID(接口可能返回字符串数字) */
+  associationId = '' as string;
+  sitesId = 0 as number;
+  level = null as number|string|null;
+  batch = 0 as number;
+  gender = '' as string;
+  basicMobile = '' as string|null;
+  basicUnit = '' as string|null;
+  ichId = 0 as number;
+  ichTitle = '' as string;
+  checkId = null as number|null;
+  userId = null as number|null;
+  year = null as number|null;
+  inheritor = '' as string|null;
+  unit = '' as string|null;
+  ichName = '' as string|null;
+  mobile = '' as string|null;
+  idCard = '' as string|null;
+  address = '' as string|null;
+  weigh = null as number|null;
+  deductPoints = null as number|null;
+  points = null as number|null;
+  self = null as number|null;
+  ichUnit = null as number|null;
+  unitPoints = null as number|null;
+  county = null as number|null;
+  countyPoints = null as number|null;
+  district = null as number|null;
+  districtPoints = null as number|null;
+  province = null as number|null;
+  provincePoints = null as number|null;
+  createtime = '' as string|null;
+  updatetime = '' as string|null;
+  /** -1=未提交,0=草稿,1=已自评,2=项目保护单位审核完成,3=县(区)文旅部门审核完成,4=设区市文旅部门/省非遗中心审核完成,5=省文化和旅游厅审核完成 */
+  progress = null as number|null;
+  flagText = '' as string;
+  typeText = '' as string;
+  openStatusText = '' as string;
+  statusText = '' as string;
+  regionText = '' as string;
+  levelText = '' as string;
+  crTypeText = '' as string;
+  ichTypeText = '' as string;
+  claimStatusText = '' as string;
+  isMultipleClaimsText = '' as string;
+  batchText = '' as string;
+  ichSiteTypeText = '' as string;
+}
+
 /** 传承人基础信息(ich/check/basic) */
 export class InheritorCheckBasicInfo extends DataModel<InheritorCheckBasicInfo> {
   constructor() {
@@ -221,15 +375,16 @@ export class SelfAssessmentDetail extends DataModel<SelfAssessmentDetail> {
       content: { 
         customToClientFn: (value) => {
           try {
+            if (typeof value === 'object') 
+              return value;
             return JSON.parse(value as string);
-          } catch (error) {
+          } catch {
             return {};
-          } 
+          }
         },
         customToServerFn: (value) => {
           return value;
         },
-
       },
       awardTime: {
         clientSide: 'date', 
@@ -376,6 +531,26 @@ export function getCheckAnnexType(mimetype: string) {
 
 export type CheckAnnexTypeValue = (typeof CheckAnnexType)[keyof typeof CheckAnnexType];
 
+/** 自查表审核提交(POST /ich/check/review) */
+export interface IchCheckReviewPayload {
+  /** 自查表 ID */
+  id: number;
+  /** 项目保护单位意见: 1=优秀,2=合格,3=不合格,4=丧失传承能力,5=取消资格 */
+  ichUnit?: number;
+  unitPoints?: number;
+  /** 县(区)文旅部门审核意见,取值同 ichUnit */
+  county?: number;
+  countyPoints?: number;
+  /** 设区市文旅部门、省非遗中心审核意见,取值同 ichUnit */
+  district?: number;
+  districtPoints?: number;
+  /** 省文化和旅游厅意见,取值同 ichUnit */
+  province?: number;
+  provincePoints?: number;
+  /** 0=草稿,1=已自评,2=项目保护单位审核完成,3=县(区)文旅部门审核完成,4=设区市文旅部门/省非遗中心审核完成,5=省文化和旅游厅审核完成 */
+  progress: number;
+}
+
 /** 证明材料修改与新增请求体(POST /ich/check/saveAnnex) */
 export interface SaveCheckAnnexPayload {
   /** 记录 ID,修改时必填 */
@@ -471,6 +646,7 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
    */
   async getSelfAssessmentList(data: {
     userId?: number;
+    progress?: number;
     level?: number;
     year?: number;
     keywords?: string;
@@ -479,6 +655,7 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
   }) {
     const res = await this.post<KeyValue>('/ich/check/getList', '评估表列表', {
       user_id: data.userId,
+      progress: data.progress,
       level: data.level,
       year: data.year,
       keywords: data.keywords,
@@ -511,6 +688,58 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
   }
 
   /**
+   * 保护单位账号:传承人传承协议分页列表
+   * POST `/ich/check/getUserAgreement`
+   * @param data.progress -1=未提交,0=草稿,1=已自评,2=审核完成
+   */
+  async getUserAgreementList(data: {
+    userId?: number;
+    progress?: number;
+    level?: number;
+    year?: number;
+    keywords?: string;
+    page?: number;
+    pageSize?: number;
+  }) {
+    const res = await this.post<KeyValue>('/ich/check/getUserAgreement', '传承人传承协议列表', {
+      user_id: data.userId,
+      progress: data.progress,
+      level: data.level,
+      year: data.year,
+      keywords: data.keywords,
+      page: data.page,
+      pageSize: data.pageSize,
+    });
+    return normalizePaginated<UserAgreementListRow>(UserAgreementListRow, res.requireData());
+  }
+
+  /**
+   * 保护单位账号:传承人分页列表
+   * POST `/ich/check/getInheritorList`
+   * @param data.progress -1=未提交,0=草稿,1=已自评,2=项目保护单位审核完成,3=县(区)文旅部门审核完成,4=设区市文旅部门/省非遗中心审核完成,5=省文化和旅游厅审核完成
+   */
+  async getInheritorList(data: {
+    userId?: number;
+    progress?: number;
+    level?: number;
+    year?: number;
+    keywords?: string;
+    page?: number;
+    pageSize?: number;
+  }) {
+    const res = await this.post<KeyValue>('/ich/check/getInheritorList', '传承人列表', {
+      user_id: data.userId,
+      progress: data.progress,
+      level: data.level,
+      year: data.year,
+      keywords: data.keywords,
+      page: data.page,
+      pageSize: data.pageSize,
+    });
+    return normalizePaginated<InheritorCheckListRow>(InheritorCheckListRow, res.requireData());
+  }
+
+  /**
    * 保存传承协议
    */
   async saveAgreement(dataModel: AgreementDetail) {
@@ -525,6 +754,25 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
   }
 
   /**
+   * 自查评估表审核
+   * POST `/ich/check/review`
+   */
+  async reviewSelfAssessment(payload: IchCheckReviewPayload) {
+    return this.post('/ich/check/review', '自查评估表审核', {
+      id: payload.id,
+      ich_unit: payload.ichUnit,
+      unit_points: payload.unitPoints,
+      county: payload.county,
+      county_points: payload.countyPoints,
+      district: payload.district,
+      district_points: payload.districtPoints,
+      province: payload.province,
+      province_points: payload.provincePoints,
+      progress: payload.progress,
+    });
+  }
+
+  /**
    * 下载自查评估表PDF
    */
   async downloadSelfAssessmentPdf(id: number) {

+ 7 - 0
src/pages.json

@@ -318,6 +318,13 @@
           }
         },
         {
+          "path": "assessment/evaluation-form-review",
+          "style": {
+            "navigationBarTitleText": "自查评估表审核",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
           "path": "assessment/evaluation-list",
           "style": {
             "navigationBarTitleText": "自查评估表列表",

+ 65 - 10
src/pages/collect/assessment/argeement-sign-list.vue

@@ -1,21 +1,32 @@
 <template>
   <FlexCol padding="space.lg" gap="gap.md">
-    <SearchBar v-model="search" @search="loader.reload()" placeholder="输入关键词搜索"  />
+    <FlexRow justify="space-between" gap="gap.sm">
+      <SearchBar v-model="search" placeholder="输入关键词搜索" :innerStyle="{ width: '400rpx' }" @search="loader.reload()" />
+      <SimpleDropDownPicker
+        v-model="progressFilter"
+        :columns="progressOptions"
+        default-text="全部状态" 
+      />
+    </FlexRow>
     <SimplePageListLoader :loader="loader">
-      <Touchable 
-        v-for="item in loader.list.value" :key="item.id"
+      <Touchable
+        v-for="item in loader.list.value"
+        :key="`${item.userId}-${item.agreementId ?? 0}`"
         direction="row"
         justify="space-between"
         backgroundColor="white"
         padding="space.md"
         radius="radius.md"
-        @click="navTo('./argeement-sign', { id: item.id, userId: item.userId })"
+        @click="openRow(item)"
       >
         <FlexCol>
-          <Text :text="item.inheritor ?? '?'" />
-          <Text :text="item.mobile || item.unit || '?'" />
+          <Text :text="item.title || item.partyB || '—'" />
+          <Text fontConfig="subText" :text="subLine(item)" />
         </FlexCol>
-        <Icon name="arrow-right-bold" />
+        <FlexRow align="center" gap="gap.sm">
+          <Text fontConfig="subText" :text="progressLabel(item.progress)" />
+          <Icon name="arrow-right-bold" />
+        </FlexRow>
       </Touchable>
     </SimplePageListLoader>
     <XBarSpace />
@@ -23,10 +34,11 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, watch } from 'vue';
 import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
 import { navTo } from '@/components/utils/PageAction';
-import AssessmentContentApi from '@/api/collect/AssessmentContent';
+import AssessmentContentApi, { type UserAgreementListRow } from '@/api/collect/AssessmentContent';
+import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import Text from '@/components/basic/Text.vue';
@@ -34,17 +46,60 @@ import Icon from '@/components/basic/Icon.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
 import Touchable from '@/components/feedback/Touchable.vue';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
 
 const search = ref('');
+const progressFilter = ref(-100);
+
+/** 与 ich/check/getUserAgreement 列表进度一致 */
+const progressOptions: SimpleDropDownPickerItem[] = [
+  { id: -100, name: '全部状态' },
+  { id: -1, name: '未提交' },
+  { id: 0, name: '草稿' },
+  { id: 1, name: '已自评' },
+  { id: 2, name: '审核完成' },
+];
+
+const agreementProgressLabels: Record<number, string> = {
+  [-1]: '未提交',
+  0: '草稿',
+  1: '已自评',
+  2: '审核完成',
+};
+
+function progressLabel(p: number | null | undefined) {
+  if (p === null || p === undefined)
+    return '未填写';
+  return agreementProgressLabels[p] ?? `未填写`;
+}
+
+function subLine(item: UserAgreementListRow) {
+  return [item.mobile, item.ichTitle].filter(Boolean).join(' · ') || '—';
+}
+
+function openRow(item: UserAgreementListRow) {
+  const id = item.agreementId && item.agreementId > 0 ? item.agreementId : 0;
+  const uid = item.userId ?? 0;
+  navTo('./argeement-sign', { id, userId: uid });
+}
+
 const loader = useSimplePageListLoader(10, async (page, pageSize) => {
-  const list = await AssessmentContentApi.getSelfAssessmentList({
+  const pv = progressFilter.value;
+  const progress = pv > -50 ? pv : undefined;
+  const list = await AssessmentContentApi.getUserAgreementList({
     year: new Date().getFullYear(),
     page,
     pageSize,
+    keywords: search.value?.trim() || undefined,
+    progress,
   });
   return {
     list: list.data,
     total: list.total,
   };
 });
+
+watch(progressFilter, () => {
+  loader.reload();
+});
 </script>

+ 2 - 2
src/pages/collect/assessment/components/AgreementBodyMunicipal.vue

@@ -89,7 +89,7 @@
       label-position="top"
       disabled
       v-model="detail.partyASign"
-      placeholder="(待正式打印填写)"
+      placeholder=""
       :show-bottom-border="true"
     />
     <Field
@@ -99,7 +99,7 @@
       type="tel"
       disabled
       v-model="detail.partyAMobile"
-      placeholder="(待正式打印填写)"
+      placeholder=""
     />
     <AgreementDateWriteBlock
       :model-value="partyAStampDate"

+ 2 - 2
src/pages/collect/assessment/components/AgreementBodyNational.vue

@@ -94,7 +94,7 @@
       label-position="top"
       disabled
       v-model="detail.partyASign"
-      placeholder="(待正式打印填写)"
+      placeholder=""
       :show-bottom-border="true"
     />
     <Field
@@ -104,7 +104,7 @@
       type="tel"
       disabled
       v-model="detail.partyAMobile"
-      placeholder="(待正式打印填写)"
+      placeholder=""
     />
     <AgreementDateWriteBlock
       :model-value="partyAStampDate"

+ 2 - 2
src/pages/collect/assessment/components/AgreementBodyProvincial.vue

@@ -93,7 +93,7 @@
       label-position="top"
       disabled
       v-model="detail.partyASign"
-      placeholder="(待正式打印填写)"
+      placeholder=""
       :show-bottom-border="true"
     />
     <Field
@@ -103,7 +103,7 @@
       type="tel"
       disabled
       v-model="detail.partyAMobile"
-      placeholder="(待正式打印填写)"
+      placeholder=""
     />
     <AgreementDateWriteBlock
       :model-value="partyAStampDate"

+ 31 - 8
src/pages/collect/assessment/components/EvaluationFormBlock.vue

@@ -2,7 +2,7 @@
   <DynamicForm
     ref="formRef"
     :model="currentForm"
-    :options="formOptions"
+    :options="mergedFormOptions"
   >
     <template #insertion="{ data }">
       <FlexCol v-if="data.name === 'insertCheckList'">
@@ -18,12 +18,14 @@
             <FlexCol v-if="item.checkType == 3" gap="gap.sm">
               <FlexRow v-for="child in item.children" :key="child.id" justify="space-between">
                 <CheckBox
+                  :disabled="readonly"
                   :text="`${child.name} (${child.points}分)`"
                   :modelValue="hasCheckedItem(child.id)" 
                   @update:modelValue="setCheckedItem(item as CheckItemInfo, child as CheckItemInfo, $event)" 
                 />
                 <Stepper 
                   v-if="hasCheckedItem(child.id)"
+                  :disabled="readonly"
                   :min="0"
                   :max="20"
                   :step="1"
@@ -37,6 +39,7 @@
             <FlexCol v-else gap="gap.sm">
               <CheckBox 
                 v-for="child in item.children" :key="child.id"
+                :disabled="readonly"
                 :text="`${child.name} (${child.points}分)`"
                 :modelValue="hasCheckedItem(child.id)" 
                 @update:modelValue="setCheckedItem(item as CheckItemInfo, child as CheckItemInfo, $event)" 
@@ -51,6 +54,7 @@
 </template>
 
 <script setup lang="ts">
+import { computed, ref } from 'vue';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import H3 from '@/components/typography/H3.vue';
@@ -59,18 +63,32 @@ import FlexRow from '@/components/layout/FlexRow.vue';
 import CheckBox from '@/components/form/CheckBox.vue';
 import Stepper from '@/components/form/Stepper.vue';
 import { SelfAssessmentCheckItemAnswer, type CheckItemInfo, type SelfAssessmentDetail } from '@/api/collect/AssessmentContent';
-import type { IDynamicFormOptions } from '@/components/dynamic';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
 import { ArrayUtils } from '@imengyu/imengyu-utils';
 import Tag from '@/components/display/Tag.vue';
 import Divider from '@/components/display/Divider.vue';
 import Height from '@/components/layout/space/Height.vue';
 
-const props = defineProps<{   
+const props = withDefaults(defineProps<{
   currentForm: SelfAssessmentDetail;
   formOptions: IDynamicFormOptions;
   checkItemList: CheckItemInfo[];
   currentFormCheckItems: SelfAssessmentCheckItemAnswer[];
-}>();
+  /** 管理员只读查看 / 审核页 */
+  readonly?: boolean;
+}>(), {
+  readonly: false,
+});
+
+const formRef = ref<IDynamicFormRef | null>(null);
+
+const mergedFormOptions = computed<IDynamicFormOptions>(() => ({
+  ...props.formOptions,
+  formAdditionaProps: {
+    ...props.formOptions.formAdditionaProps,
+    disabled: props.readonly,
+  },
+}));
 
 
 function getCheckModeText(checkMode: number) {
@@ -87,17 +105,14 @@ function hasCheckedItem(id: number) {
   return props.currentFormCheckItems.some(item => item.id === id);
 }
 function getCheckedItemCount(id: number) {
-  console.log('getCheckedItemCount', id);
   return props.currentFormCheckItems.find(item => item.id === id)?.count;
 }
 function setCheckedItem(checkItem: CheckItemInfo, childItem: CheckItemInfo, count: number|boolean) {
-  if (!props.currentForm)
+  if (props.readonly)
     return;
-  console.log('setCheckedItem', checkItem, childItem, count);
   if (typeof count === 'boolean') {
     count = count ? 1 : 0;
   }
-  console.log('setCheckedItem', childItem.id, count);
   let item = props.currentFormCheckItems.find(item => item.id === childItem.id);
   if (!item) {
     item = new SelfAssessmentCheckItemAnswer();
@@ -126,4 +141,12 @@ function setCheckedItem(checkItem: CheckItemInfo, childItem: CheckItemInfo, coun
   if (item.count === 0)
     ArrayUtils.remove(props.currentFormCheckItems, item);
 }
+
+async function validate() {
+  if (props.readonly)
+    return;
+  await formRef.value?.validate();
+}
+
+defineExpose({ validate });
 </script>

+ 68 - 0
src/pages/collect/assessment/components/SelfAssessmentFormDisplay.vue

@@ -0,0 +1,68 @@
+<template>
+  <FlexCol gap="gap.lg">
+    <EvaluationFormBlock
+      ref="blockRef"
+      :current-form="currentForm"
+      :form-options="formOptions"
+      :check-item-list="checkItemList"
+      :current-form-check-items="currentFormCheckItems"
+      :readonly="readonly"
+    />
+    <Divider />
+    <FlexRow align="flex-end" justify="space-between">
+      <H3>自评总分</H3>
+      <Text fontSize="50rpx" color="#315816" fontFamily="HUNdin1451" :text="`${totalPoints}分`" />
+    </FlexRow>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import type { CheckItemInfo, SelfAssessmentCheckItemAnswer, SelfAssessmentDetail } from '@/api/collect/AssessmentContent';
+import type { IDynamicFormOptions } from '@/components/dynamic';
+import EvaluationFormBlock from './EvaluationFormBlock.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Divider from '@/components/display/Divider.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import H3 from '@/components/typography/H3.vue';
+import Text from '@/components/basic/Text.vue';
+
+const props = withDefaults(defineProps<{
+  currentForm: SelfAssessmentDetail;
+  formOptions: IDynamicFormOptions;
+  checkItemList: CheckItemInfo[];
+  currentFormCheckItems: SelfAssessmentCheckItemAnswer[];
+  readonly?: boolean;
+}>(), {
+  readonly: false,
+});
+
+const blockRef = ref<InstanceType<typeof EvaluationFormBlock> | null>(null);
+
+const checkItemMap = computed(() => {
+  const m = new Map<number, CheckItemInfo>();
+  function walk(nodes: CheckItemInfo[]) {
+    for (const n of nodes) {
+      m.set(n.id, n);
+      if (n.children?.length)
+        walk(n.children);
+    }
+  }
+  walk(props.checkItemList);
+  return m;
+});
+
+const totalPoints = computed(() => {
+  return props.currentFormCheckItems
+    .filter((item) => {
+      const checkItem = checkItemMap.value.get(item.id);
+      return checkItem && !checkItem.isTitle;
+    })
+    .reduce((acc, item) => acc + (item.count * item.points), 0)
+    - (props.currentForm.deductPoints ?? 0);
+});
+
+defineExpose({
+  validate: () => blockRef.value?.validate(),
+});
+</script>

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

@@ -0,0 +1,304 @@
+<template>
+  <CommonRoot>
+    <FlexCol padding="space.lg">
+      <SimplePageContentLoader :loader="loader">
+        <template v-if="loader.isFinished.value">
+          <Result
+            v-if="!currentForm"
+            status="warning"
+            title="无法加载评估表"
+            description="请从自查评估表列表进入,并确认参数正确。"
+          />
+          <FlexCol v-else gap="gap.lg">
+            <Alert type="info" :message="progressHint" />
+            <SelfAssessmentFormDisplay
+              :current-form="currentForm"
+              :form-options="formOptions"
+              :check-item-list="checkItemList"
+              :current-form-check-items="currentFormCheckItems"
+              :readonly="true"
+            />
+            <Divider />
+            <H3>佐证资料</H3>
+            <Result v-if="!annexLinks.length" status="info" title="暂无佐证资料" />
+            <FlexCol v-else gap="gap.sm">
+              <Touchable
+                v-for="a in annexLinks"
+                :key="a.id"
+                padding="space.sm"
+                backgroundColor="white"
+                radius="radius.md"
+                @click="copyAnnexUrl(a)"
+              >
+                <Text :text="a.name" />
+              </Touchable>
+            </FlexCol>
+            <Divider />
+            <H3>审核提交</H3>
+            <FlexCol gap="gap.md">
+              <Text bold :text="`审核环节:${reviewLevelLabel}`" />
+              <Text fontConfig="subText" text="审核意见(必选)" />
+              <FlexRow wrap gap="gap.sm">
+                <Button
+                  v-for="o in opinionSelectOptions"
+                  :key="o.value"
+                  :type="reviewOpinion === o.value ? 'primary' : 'default'"
+                  size="small"
+                  @click="reviewOpinion = o.value"
+                >
+                  {{ o.label }}
+                </Button>
+              </FlexRow>
+              <Text fontConfig="subText" text="评分(可选,0–100)" />
+              <Stepper
+                :min="0"
+                :max="100"
+                :step="1"
+                :model-value="reviewPoints ?? 0"
+                @update:model-value="reviewPoints = $event"
+              />
+              <Text fontConfig="subText" text="提交后进度" />
+              <FlexRow wrap gap="gap.sm">
+                <Button
+                  v-for="o in progressSubmitOptions"
+                  :key="o.value"
+                  :type="submitProgress === o.value ? 'primary' : 'default'"
+                  size="small"
+                  @click="submitProgress = o.value"
+                >
+                  {{ o.label }}
+                </Button>
+              </FlexRow>
+              <Button type="primary" block :loading="submitLoading" @click="submitReview">
+                提交审核
+              </Button>
+            </FlexCol>
+          </FlexCol>
+        </template>
+      </SimplePageContentLoader>
+      <XBarSpace />
+    </FlexCol>
+  </CommonRoot>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { formatError, waitTimeOut } from '@imengyu/imengyu-utils';
+import { toast, alert } from '@/components/dialog/CommonRoot';
+import AssessmentContentApi, {
+  type CheckItemInfo,
+  type SelfAssessmentCheckItemAnswer,
+  SelfAssessmentDetail,
+} from '@/api/collect/AssessmentContent';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import Result from '@/components/feedback/Result.vue';
+import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vue';
+import { buildSelfAssessmentFormOptions } from './evaluationFormOptions';
+import { useImageSimpleUploadCo } from '@/common/components/upload/ImageUploadCo';
+import Divider from '@/components/display/Divider.vue';
+import H3 from '@/components/typography/H3.vue';
+import Text from '@/components/basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import Button from '@/components/basic/Button.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Stepper from '@/components/form/Stepper.vue';
+import Alert from '@/components/feedback/Alert.vue';
+import XBarSpace from '@/components/layout/space/XBarSpace.vue';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+
+const signUploadCo = useImageSimpleUploadCo();
+const formOptions = buildSelfAssessmentFormOptions(signUploadCo);
+
+const currentForm = ref<SelfAssessmentDetail | null>(null);
+const currentFormCheckItems = ref<SelfAssessmentCheckItemAnswer[]>([]);
+const checkItemList = ref<CheckItemInfo[]>([]);
+const annexLinks = ref<{ id: number; name: string; url: string }[]>([]);
+
+const submitLoading = ref(false);
+const reviewOpinion = ref<number | null>(null);
+const reviewPoints = ref<number | null>(null);
+const submitProgress = ref(2);
+
+const opinionSelectOptions = [
+  { label: '优秀', value: 1 },
+  { label: '合格', value: 2 },
+  { label: '不合格', value: 3 },
+  { label: '丧失传承能力', value: 4 },
+  { label: '取消资格', value: 5 },
+];
+
+const progressSubmitOptions = [
+  { label: '2 保护单位完成', value: 2 },
+  { label: '3 县区完成', value: 3 },
+  { label: '4 市/省中心完成', value: 4 },
+  { label: '5 省厅完成', value: 5 },
+];
+
+const { querys } = useLoadQuerys({
+  id: 0,
+  userId: 0,
+  progress: Number.NaN,
+}, () => {
+  loader.load();
+});
+
+const listProgress = computed(() => {
+  const p = querys.value.progress;
+  return Number.isFinite(p) ? p : Number.NaN;
+});
+
+const targetProgressDefault = computed(() => {
+  const p = listProgress.value;
+  if (!Number.isFinite(p) || p < 1)
+    return 2;
+  if (p >= 5)
+    return 5;
+  return p + 1;
+});
+
+const reviewLevelLabel = computed(() => {
+  switch (submitProgress.value) {
+    case 2:
+      return '项目保护单位';
+    case 3:
+      return '县(区)文旅部门';
+    case 4:
+      return '设区市文旅部门、省非遗中心';
+    case 5:
+      return '省文化和旅游厅';
+    default:
+      return '';
+  }
+});
+
+const progressHint = computed(() => {
+  const id = querys.value.id;
+  const uid = querys.value.userId;
+  const lp = listProgress.value;
+  const lpText = Number.isFinite(lp) ? `列表进度:${lp}` : '未传列表进度,已按默认环节';
+  return `自查表 ${id},传承人用户 ${uid}。${lpText}。提交后进度:${submitProgress.value}。`;
+});
+
+const currentYear = new Date().getFullYear();
+
+function levelTitleFromForm(f: SelfAssessmentDetail) {
+  if (f.level === 23) return '国家级';
+  if (f.level === 24) return '省级';
+  if (f.level === 25) return '市级';
+  return '国家级';
+}
+
+function loadEditorContent(f: SelfAssessmentDetail) {
+  if (typeof f.content !== 'object' || f.content === null)
+    f.content = {};
+  f.content.title = `传承人填写${currentYear}年1月1日至${currentYear}年12月31日${levelTitleFromForm(f)}非遗传承人义务履行和传承补助经费使用情况等,不超过1000字,如未履行职责请进行说明。参考提纲如下:`;
+  for (let i = 0; i < 8; i++) {
+    if (typeof f.content[`item${i}`] !== 'string')
+      f.content[`item${i}`] = '';
+  }
+}
+
+async function loadCheckItems(f: SelfAssessmentDetail) {
+  const { top } = await AssessmentContentApi.getCheckItems(Number(f.level));
+  checkItemList.value = top;
+  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 = submitProgress.value;
+  if (t === 2) {
+    reviewOpinion.value = f.ichUnit ?? null;
+    reviewPoints.value = f.unitPoints || null;
+  } else if (t === 3) {
+    reviewOpinion.value = f.county ?? null;
+    reviewPoints.value = f.countyPoints || null;
+  } else if (t === 4) {
+    reviewOpinion.value = f.district ?? null;
+    reviewPoints.value = f.districtPoints || null;
+  } else if (t === 5) {
+    reviewOpinion.value = f.province ?? null;
+    reviewPoints.value = f.provincePoints || null;
+  }
+}
+
+function copyAnnexUrl(a: { name: string; url: string }) {
+  uni.setClipboardData({
+    data: a.url,
+    success: () => {
+      toast(`已复制「${a.name}」链接`);
+    },
+  });
+}
+
+const loader = useSimpleDataLoader(async () => {
+  const id = querys.value.id;
+  const uid = querys.value.userId;
+  if (!id || !uid) {
+    currentForm.value = null;
+    return null;
+  }
+  const detail = await AssessmentContentApi.getSelfAssessmentDetail(id, uid);
+  currentForm.value = detail;
+  loadEditorContent(detail);
+  await loadCheckItems(detail);
+  if (detail.id)
+    await loadAnnexLinks(detail.id);
+  else
+    annexLinks.value = [];
+  submitProgress.value = targetProgressDefault.value;
+  applyPrefillFromDetail(detail);
+  return detail;
+}, false);
+
+watch(submitProgress, () => {
+  const f = currentForm.value;
+  if (f)
+    applyPrefillFromDetail(f);
+});
+
+async function submitReview() {
+  const f = currentForm.value;
+  if (!f?.id) {
+    toast('缺少自查表 ID');
+    return;
+  }
+  if (reviewOpinion.value == null) {
+    toast('请选择审核意见');
+    return;
+  }
+  const t = submitProgress.value;
+  const base = { id: f.id, progress: t };
+  const points = reviewPoints.value != null && reviewPoints.value >= 0 ? reviewPoints.value : undefined;
+  const op = reviewOpinion.value;
+  submitLoading.value = true;
+  try {
+    if (t === 2)
+      await AssessmentContentApi.reviewSelfAssessment({ ...base, ichUnit: op, unitPoints: points });
+    else if (t === 3)
+      await AssessmentContentApi.reviewSelfAssessment({ ...base, county: op, countyPoints: points });
+    else if (t === 4)
+      await AssessmentContentApi.reviewSelfAssessment({ ...base, district: op, districtPoints: points });
+    else
+      await AssessmentContentApi.reviewSelfAssessment({ ...base, province: op, provincePoints: points });
+    toast('审核提交成功');
+    await waitTimeOut(400);
+    uni.navigateBack();
+  } catch (e) {
+    alert({ title: '审核提交失败', content: formatError(e) });
+  } finally {
+    submitLoading.value = false;
+  }
+}
+</script>

+ 16 - 313
src/pages/collect/assessment/evaluation-form.vue

@@ -12,18 +12,15 @@
             <Button type="primary" @click="createForm">去填写评估表</Button>
           </Result>
           <FlexCol v-else gap="gap.lg">
-            <EvaluationFormBlock
-              :currentForm="(currentForm as SelfAssessmentDetail)"
-              :formOptions="formOptions"
-              :checkItemList="(checkItemList as CheckItemInfo[])"
-              :currentFormCheckItems="(currentFormCheckItems as SelfAssessmentCheckItemAnswer[])"
+            <SelfAssessmentFormDisplay
+              ref="blockRef"
+              :current-form="(currentForm as SelfAssessmentDetail)"
+              :form-options="formOptions"
+              :check-item-list="(checkItemList as CheckItemInfo[])"
+              :current-form-check-items="(currentFormCheckItems as SelfAssessmentCheckItemAnswer[])"
+              :readonly="false"
             />
             <Divider />
-            <FlexRow align="flex-end" justify="space-between">
-              <H3>自评总分</H3>
-              <Text fontSize="50rpx" color="#315816" fontFamily="HUNdin1451" :text="`${totalPoints}分`" />
-            </FlexRow>
-            <Divider />
             <FlexCol gap="gap.lg">
               <FlexCol v-for="(title, secIdx) in externalReviewSectionTitles" :key="secIdx" gap="gap.sm">
                 <Text bold :text="title.title" />
@@ -85,7 +82,7 @@ import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLo
 import { useAuthStore } from '@/store/auth';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
 import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
-import { assertNotNull, formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
+import { assertNotNull, formatError, waitTimeOut } from '@imengyu/imengyu-utils';
 import { toast, alert } from '@/components/dialog/CommonRoot';
 import AssessmentContentApi, {
   SelfAssessmentDetail,
@@ -103,18 +100,15 @@ import H3 from '@/components/typography/H3.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Text from '@/components/basic/Text.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
-import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
-import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
-import type { FieldProps } from '@/components/form/Field.vue';
-import type { SignatureFieldProps } from '@/components/form/SignatureField.vue';
-import { useImageSimpleUploadCo } from '@/common/components/upload/ImageUploadCo';
-import EvaluationFormBlock from './components/EvaluationFormBlock.vue';
 import Uploader, { type UploaderInstance } from '@/components/form/Uploader.vue';
 import { getMimeType } from '@/common/components/upload/mimes';
 import Divider from '@/components/display/Divider.vue';
 import { stringUrlToUploaderItem } from '@/components/form/Uploader';
 import CheckBox from '@/components/form/CheckBox.vue';
 import Field from '@/components/form/Field.vue';
+import SelfAssessmentFormDisplay from './components/SelfAssessmentFormDisplay.vue';
+import { buildSelfAssessmentFormOptions } from './evaluationFormOptions';
+import { useImageSimpleUploadCo } from '@/common/components/upload/ImageUploadCo';
 
 /** 评估表下方展示用:各级审核意见(不接数据、禁用) */
 const externalReviewSectionTitles = ref([
@@ -159,301 +153,11 @@ const assessmentAnnexUpload = useAliOssUploadCo('assessment/annex', async (res,
 const currentYear = new Date().getFullYear();
 
 const uploaderRef = ref<UploaderInstance | null>(null);
-const formRef = ref<IDynamicFormRef | null>(null);
-const formOptions : IDynamicFormOptions = {
-  formAdditionaProps: {
-    labelFlex: 4,
-    inputFlex: 8,
-  },
-  formItems: [
-    {
-      type: 'flat-group',
-      label: '传承人自查评估',
-      name: 'selfAssessmentGroup',
-      childrenColProps: { span: 24 },
-      children: [
-        {
-          label: '传承人名称',
-          name: 'inheritor',
-          type: 'text',
-          additionalProps: { placeholder: '请输入传承人名称' },
-        },
-        {
-          label: '项目保护单位',
-          name: 'unit',
-          type: 'text',
-          additionalProps: { placeholder: '请输入项目保护单位' },
-        },
-        {
-          label: '项目名称',
-          name: 'ichName',
-          type: 'text',
-          additionalProps: { placeholder: '请输入项目名称' },
-        },
-        {
-          label: '联系电话',
-          name: 'mobile',
-          type: 'text',
-          additionalProps: { placeholder: '请输入联系电话' },
-        },
-        {
-          label: '身份证号',
-          name: 'idCard',
-          type: 'text',
-          additionalProps: { placeholder: '请输入身份证号' },
-        },
-        {
-          label: '级别',
-          name: 'level',
-          type: 'select-id',
-          additionalProps: {
-            placeholder: '请选择级别',
-            loadData: async () => [
-              { text: '国家级', value: 23 },
-              { text: '省级', value: 24 },
-              { text: '市级', value: 25 },
-            ],
-          },
-          formProps: {
-            showRightArrow: true,
-          },
-        },
-        {
-          label: '家庭住址',
-          name: 'address',
-          type: 'text',
-          additionalProps: { placeholder: '请输入家庭住址' },
-        },
-        {
-          label: '获评时间',
-          name: 'awardTime',
-          type: 'date',
-          additionalProps: { 
-            placeholder: '请选择获评时间',
-            shouldUpdateValueImmediately: true,
-          },
-          formProps: {
-            showRightArrow: true,
-          },
-        },
-        {
-          label: '自评报告',
-          name: 'content',
-          type: 'object',
-          children: [
-            {
-              label: '',
-              name: 'title',
-              type: 'static-text',
-            },
-            {
-              label: '(一)开展传承活动,培养后继人才情况;',
-              name: 'item0',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(二)妥善保存相关实物、资料情况;',
-              name: 'item1',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(三)配合进行非物质文化遗产调查情况;',
-              name: 'item2',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(四)参加非物质文化遗产公益性宣传情况;',
-              name: 'item3',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(五)补助经费使用情况;',
-              name: 'item4',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(六)参加培训情况',
-              name: 'item5',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            },
-            {
-              label: '(七)其他相关情况;',
-              name: 'item6',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              } as FieldProps,
-            },  
-            {
-              label: '(八)存在的问题及原因分析。',
-              name: 'item7',
-              type: 'textarea',
-              formProps: {
-                labelPosition: 'top',
-              },
-              additionalProps: { 
-                placeholder: '请填写',
-                maxLength: 1000,
-                colon: false,
-                showWordLimit: true,
-              },
-            }
-          ],
-        },
-      ],
-    },
-    {
-      type: 'insertion',
-      name: 'insertCheckList',
-    },
-    {
-      type: 'flat-group',
-      label: '传承人自查评估',
-      name: 'selfAssessmentGroup2',
-      childrenColProps: { span: 24 },
-      children: [
-        {
-          label: '其他相关情况(扣分内容)',
-          name: 'deductContent',
-          type: 'text',
-          additionalProps: {
-            showWordLimit: true,
-            maxlength: 260,
-            placeholder: '请输入其他相关情况(扣分内容)',
-          } as FieldProps,
-        },
-        {
-          label: '其他相关情况(扣分分值)',
-          name: 'deductPoints',
-          type: 'number',
-          additionalProps: {
-            placeholder: '请输入其他相关情况(扣分分值)',
-            min: 0,
-            max: 100,
-          },
-        },
-        {
-          label: '自我评估',
-          name: 'self',
-          type: 'radio-value',
-          additionalProps: {
-            options: [
-              { text: '优秀', value: 1 },
-              { text: '合格', value: 2 },
-              { text: '不合格', value: 3 },
-              { text: '丧失传承能力', value: 4 },
-              { text: '取消资格', value: 5 },
-            ],
-            vertical: true,
-          } as RadioValueProps,
-        },
-        {
-          label: '传承人签名',
-          name: 'inheritorSign',
-          type: 'sign',
-          formProps: {
-            showRightArrow: true,
-          },
-          additionalProps: {
-            upload: useImageSimpleUploadCo(),
-            previewImageProps: {
-              width: '400rpx',
-            }
-          } as SignatureFieldProps,
-        }
-      ],
-    },
-  ],
-  formRules: {
-    inheritor: [{ required: true, message: '请输入传承人名称' }],
-    unit: [{ required: true, message: '请输入项目保护单位' }],
-    ichName: [{ required: true, message: '请输入项目名称' }],
-    mobile: [{ required: true, message: '请输入联系电话' }],
-    idCard: [{ required: true, message: '请输入身份证号' }],
-    level: [{ required: true, message: '请选择级别' }],
-    address: [{ required: true, message: '请输入家庭住址' }],
-    content: [{ required: true, message: '请填写自评报告' }],
-    self: [{ required: true, message: '请选择自我评估' }],
-    sign: [{ required: true, message: '请传承人签名' }],
-    awardTime: [{ required: true, message: '请选择获评时间' }],
-  },
-};
+const blockRef = ref<InstanceType<typeof SelfAssessmentFormDisplay> | null>(null);
+const signUploadCo = useImageSimpleUploadCo();
+const formOptions = buildSelfAssessmentFormOptions(signUploadCo);
 
 const checkItemList = ref<CheckItemInfo[]>([]);
-const editorTitle = ref('');
-let checkItemMap = new Map<number, CheckItemInfo>();
-
-const totalPoints = computed(() => {
-  if (!currentForm.value) 
-    return 0;
-  return currentFormCheckItems.value
-    .filter((item) => {
-      const checkItem = checkItemMap.get(item.id);
-      return checkItem && !checkItem.isTitle;
-    })
-    .reduce((acc, item) => acc + (item.count * item.points), 0)
-    - (currentForm.value.deductPoints ?? 0);
-});
 const levelTitle = computed(() => {
   if (currentForm.value?.level === 23) return '国家级';
   if (currentForm.value?.level === 24) return '省级';
@@ -487,9 +191,8 @@ async function loadBasicInfo() {
 }
 async function loadCheckItems() {
   assertNotNull(currentForm.value, 'currentForm is null');
-  const { top, map } = await AssessmentContentApi.getCheckItems(Number(currentForm.value.level));
+  const { top } = await AssessmentContentApi.getCheckItems(Number(currentForm.value.level));
   checkItemList.value = top;
-  checkItemMap = map;
   currentFormCheckItems.value = currentForm.value.checkItems.concat();
 }
 async function loadAnnexList() {
@@ -518,7 +221,7 @@ async function createForm() {
 async function saveForm() {
   const detail = currentForm.value;
   try {
-    await formRef.value?.validate();
+    await blockRef.value?.validate();
   } catch (error) {
     toast('请填写完整信息');
     return;

+ 106 - 14
src/pages/collect/assessment/evaluation-list.vue

@@ -1,50 +1,142 @@
 <template>
   <FlexCol padding="space.lg" gap="gap.md">
-    <SearchBar v-model="search" @search="loader.reload()" placeholder="输入关键词搜索"  />
+    <FlexRow justify="space-between" gap="gap.sm">
+      <SearchBar v-model="search" placeholder="输入关键词搜索" :innerStyle="{ width: '400rpx' }" @search="loader.reload()" />
+      <SimpleDropDownPicker
+        v-model="progressFilter"
+        :columns="progressOptions"
+        default-text="全部状态"
+      />
+    </FlexRow>
     <SimplePageListLoader :loader="loader">
-      <Touchable 
-        v-for="item in loader.list.value" :key="item.id"
-        direction="row"
+      <FlexRow
+        v-for="item in loader.list.value"
+        :key="`${item.userId}-${item.checkId ?? item.id}`"
+        align="center"
+        justify="space-between"
+        gap="gap.sm"
         backgroundColor="white"
         padding="space.md"
         radius="radius.md"
-        justify="space-between"
-        @click="navTo('./evaluation-form', { id: item.id, userId: item.userId })"
       >
-        <FlexCol>
-          <Text :text="item.inheritor ?? '?'" />
-          <Text :text="item.mobile || item.unit || '?'" />
-        </FlexCol>
+        <Touchable
+          flex="1"
+          direction="row"
+          justify="space-between"
+          @click="openEdit(item)"
+        >
+          <FlexCol flex="1">
+            <Text :text="item.inheritor ?? item.title ?? '?'" />
+            <Text fontConfig="subText" :text="subLine(item)" />
+          </FlexCol>
+        </Touchable>
+        <Text fontConfig="subText" :text="progressLabel(item.progress)" />
+        <Button
+          v-if="canReview(item)"
+          type="primary"
+          size="small"
+          @click.stop="openReview(item)"
+        >
+          审核
+        </Button>
         <Icon name="arrow-right-bold" />
-      </Touchable>
+      </FlexRow>
     </SimplePageListLoader>
     <XBarSpace />
   </FlexCol>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, watch } from 'vue';
 import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
 import { navTo } from '@/components/utils/PageAction';
-import AssessmentContentApi from '@/api/collect/AssessmentContent';
+import AssessmentContentApi, { type InheritorCheckListRow } from '@/api/collect/AssessmentContent';
+import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import Text from '@/components/basic/Text.vue';
 import Icon from '@/components/basic/Icon.vue';
 import SearchBar from '@/components/form/SearchBar.vue';
 import Touchable from '@/components/feedback/Touchable.vue';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
+import Button from '@/components/basic/Button.vue';
 
 const search = ref('');
+/** 与接口一致:-100=全部,其余为 progress 精确筛选 */
+const progressFilter = ref(-100);
+
+const progressOptions: SimpleDropDownPickerItem[] = [
+  { 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 progressLabels: Record<number, string> = {
+  [-1]: '未提交',
+  0: '草稿',
+  1: '已自评',
+  2: '项目保护单位审核完成',
+  3: '县(区)文旅部门审核完成',
+  4: '设区市/省非遗中心审核完成',
+  5: '省文化和旅游厅审核完成',
+};
+
+function progressLabel(p: number | null | undefined) {
+  if (p === null || p === undefined)
+    return '未填写';
+  return progressLabels[p] ?? `未填写`;
+}
+
+function subLine(item: InheritorCheckListRow) {
+  return [item.mobile, item.unit, item.ichTitle].filter(Boolean).join(' · ') || '—';
+}
+
+function canReview(item: InheritorCheckListRow) {
+  return Boolean(item.checkId && item.userId != null && item.progress != null && item.progress >= 1 && item.progress < 5);
+}
+
+function openEdit(item: InheritorCheckListRow) {
+  const cid = item.checkId ?? item.id;
+  const uid = item.userId ?? 0;
+  navTo('./evaluation-form', { id: cid, userId: uid });
+}
+
+function openReview(item: InheritorCheckListRow) {
+  const cid = item.checkId;
+  const uid = item.userId;
+  if (!cid || uid == null) {
+    return;
+  }
+  navTo('./evaluation-form-review', {
+    id: cid,
+    userId: uid,
+    progress: item.progress ?? '',
+  });
+}
+
 const loader = useSimplePageListLoader(10, async (page, pageSize) => {
-  const list = await AssessmentContentApi.getSelfAssessmentList({
+  const pv = progressFilter.value;
+  const progress = pv > -50 ? pv : undefined;
+  const list = await AssessmentContentApi.getInheritorList({
     year: new Date().getFullYear(),
     page,
     pageSize,
+    keywords: search.value?.trim() || undefined,
+    progress,
   });
   return {
     list: list.data,
     total: list.total,
   };
 });
+
+watch(progressFilter, () => {
+  loader.reload();
+});
 </script>

+ 273 - 0
src/pages/collect/assessment/evaluationFormOptions.ts

@@ -0,0 +1,273 @@
+import type { IDynamicFormOptions } from '@/components/dynamic';
+import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
+import type { FieldProps } from '@/components/form/Field.vue';
+import type { SignatureFieldProps } from '@/components/form/SignatureField.vue';
+import { useImageSimpleUploadCo } from '@/common/components/upload/ImageUploadCo';
+
+/** 与 `evaluation-form` 页一致的动态表单配置(供编辑页与只读展示复用) */
+export function buildSelfAssessmentFormOptions(
+  signUpload: ReturnType<typeof useImageSimpleUploadCo>,
+): IDynamicFormOptions {
+  return {
+    formAdditionaProps: {
+      labelFlex: 4,
+      inputFlex: 8,
+    },
+    formItems: [
+      {
+        type: 'flat-group',
+        label: '传承人自查评估',
+        name: 'selfAssessmentGroup',
+        childrenColProps: { span: 24 },
+        children: [
+          {
+            label: '传承人名称',
+            name: 'inheritor',
+            type: 'text',
+            additionalProps: { placeholder: '请输入传承人名称' },
+          },
+          {
+            label: '项目保护单位',
+            name: 'unit',
+            type: 'text',
+            additionalProps: { placeholder: '请输入项目保护单位' },
+          },
+          {
+            label: '项目名称',
+            name: 'ichName',
+            type: 'text',
+            additionalProps: { placeholder: '请输入项目名称' },
+          },
+          {
+            label: '联系电话',
+            name: 'mobile',
+            type: 'text',
+            additionalProps: { placeholder: '请输入联系电话' },
+          },
+          {
+            label: '身份证号',
+            name: 'idCard',
+            type: 'text',
+            additionalProps: { placeholder: '请输入身份证号' },
+          },
+          {
+            label: '级别',
+            name: 'level',
+            type: 'select-id',
+            additionalProps: {
+              placeholder: '请选择级别',
+              loadData: async () => [
+                { text: '国家级', value: 23 },
+                { text: '省级', value: 24 },
+                { text: '市级', value: 25 },
+              ],
+            },
+            formProps: {
+              showRightArrow: true,
+            },
+          },
+          {
+            label: '家庭住址',
+            name: 'address',
+            type: 'text',
+            additionalProps: { placeholder: '请输入家庭住址' },
+          },
+          {
+            label: '获评时间',
+            name: 'awardTime',
+            type: 'date',
+            additionalProps: {
+              placeholder: '请选择获评时间',
+              shouldUpdateValueImmediately: true,
+            },
+            formProps: {
+              showRightArrow: true,
+            },
+          },
+          {
+            label: '自评报告',
+            name: 'content',
+            type: 'object',
+            children: [
+              {
+                label: '',
+                name: 'title',
+                type: 'static-text',
+              },
+              {
+                label: '(一)开展传承活动,培养后继人才情况;',
+                name: 'item0',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(二)妥善保存相关实物、资料情况;',
+                name: 'item1',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(三)配合进行非物质文化遗产调查情况;',
+                name: 'item2',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(四)参加非物质文化遗产公益性宣传情况;',
+                name: 'item3',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(五)补助经费使用情况;',
+                name: 'item4',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(六)参加培训情况',
+                name: 'item5',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+              {
+                label: '(七)其他相关情况;',
+                name: 'item6',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                } as FieldProps,
+              },
+              {
+                label: '(八)存在的问题及原因分析。',
+                name: 'item7',
+                type: 'textarea',
+                formProps: { labelPosition: 'top' },
+                additionalProps: {
+                  placeholder: '请填写',
+                  maxLength: 1000,
+                  colon: false,
+                  showWordLimit: true,
+                },
+              },
+            ],
+          },
+        ],
+      },
+      {
+        type: 'insertion',
+        name: 'insertCheckList',
+      },
+      {
+        type: 'flat-group',
+        label: '传承人自查评估',
+        name: 'selfAssessmentGroup2',
+        childrenColProps: { span: 24 },
+        children: [
+          {
+            label: '其他相关情况(扣分内容)',
+            name: 'deductContent',
+            type: 'text',
+            additionalProps: {
+              showWordLimit: true,
+              maxlength: 260,
+              placeholder: '请输入其他相关情况(扣分内容)',
+            } as FieldProps,
+          },
+          {
+            label: '其他相关情况(扣分分值)',
+            name: 'deductPoints',
+            type: 'number',
+            additionalProps: {
+              placeholder: '请输入其他相关情况(扣分分值)',
+              min: 0,
+              max: 100,
+            },
+          },
+          {
+            label: '自我评估',
+            name: 'self',
+            type: 'radio-value',
+            additionalProps: {
+              options: [
+                { text: '优秀', value: 1 },
+                { text: '合格', value: 2 },
+                { text: '不合格', value: 3 },
+                { text: '丧失传承能力', value: 4 },
+                { text: '取消资格', value: 5 },
+              ],
+              vertical: true,
+            } as RadioValueProps,
+          },
+          {
+            label: '传承人签名',
+            name: 'inheritorSign',
+            type: 'sign',
+            formProps: {
+              showRightArrow: true,
+            },
+            additionalProps: {
+              upload: signUpload,
+              previewImageProps: {
+                width: '400rpx',
+              },
+            } as SignatureFieldProps,
+          },
+        ],
+      },
+    ],
+    formRules: {
+      inheritor: [{ required: true, message: '请输入传承人名称' }],
+      unit: [{ required: true, message: '请输入项目保护单位' }],
+      ichName: [{ required: true, message: '请输入项目名称' }],
+      mobile: [{ required: true, message: '请输入联系电话' }],
+      idCard: [{ required: true, message: '请输入身份证号' }],
+      level: [{ required: true, message: '请选择级别' }],
+      address: [{ required: true, message: '请输入家庭住址' }],
+      content: [{ required: true, message: '请填写自评报告' }],
+      self: [{ required: true, message: '请选择自我评估' }],
+      sign: [{ required: true, message: '请传承人签名' }],
+      awardTime: [{ required: true, message: '请选择获评时间' }],
+    },
+  };
+}

+ 1 - 1
src/pages/collect/login.vue

@@ -104,7 +104,7 @@ const formDefine: IDynamicFormOptions = {
       additionalProps: {
         options: [
           { text: '传承人', value: 0 },
-          { text: '管理员', value: 1 },
+          { text: '管理员/保护单位', value: 1 },
         ],
       } as RadioValueProps,
     },