Ver código fonte

📦 采集系统基础页

快乐的梦鱼 1 mês atrás
pai
commit
2091a1a2aa

+ 1 - 1
package-lock.json

@@ -6553,7 +6553,7 @@
     },
     "node_modules/ali-oss": {
       "version": "6.23.0",
-      "resolved": "https://registry.npmjs.org/ali-oss/-/ali-oss-6.23.0.tgz",
+      "resolved": "https://registry.npmmirror.com/ali-oss/-/ali-oss-6.23.0.tgz",
       "integrity": "sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==",
       "dev": true,
       "license": "MIT",

+ 11 - 11
src/api/auth/CollectUserApi.ts

@@ -1,4 +1,4 @@
-import { DataModel } from '@imengyu/js-request-transform';
+import { DataModel, transformDataModel, type KeyValue } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
 import { LoginResult } from './UserApi';
 
@@ -30,7 +30,16 @@ export class CollectUserApi extends AppServerRequestModule<DataModel> {
     mobile: string,
     password: string,
   }) {
-    return (await this.post('/ich/inheritor/login', '登录', data, undefined, LoginResult)).data as LoginResult;
+    const res = await this.post<KeyValue>('/ich/inheritor/login', '登录', data);
+    return transformDataModel<LoginResult>(LoginResult, {
+      mainBodyUserInfo: {
+        ...res.data,
+        main_body_id: 1,
+      },
+      auth: {
+        ...res.data,
+      },
+    });
   }
   async loginAdmin(data: {
     account: string,
@@ -41,15 +50,6 @@ export class CollectUserApi extends AppServerRequestModule<DataModel> {
       password: data?.password,
     }, undefined, LoginResult)).data as LoginResult;
   }
-  async updatePassword(data: {
-    newpassword: string,
-    oldpassword: string,
-  }) {
-    return (await this.post('/content/main_body_user/changepwd', '更新密码', data))
-  }
-  async refresh() {
-    return (await this.post('/ich/inheritor/refresh', '刷新token', {}, undefined, LoginResult)).data as LoginResult;
-  }
 }
 
 export default new CollectUserApi();

+ 6 - 0
src/api/auth/UserApi.ts

@@ -107,6 +107,12 @@ export class UserApi extends AppServerRequestModule<DataModel> {
   }) {
     return (await this.post('/content/main_body_user/editMainBodyUser', '更新用户信息', data))
   }
+  async updatePassword(data: {
+    newpassword: string,
+    oldpassword: string,
+  }) {
+    return (await this.post('/content/main_body_user/changepwd', '更新密码', data))
+  }
   async refresh() {
     return (await this.post('/content/main_body_user/refreshUser', '刷新用户', {
     }, undefined, LoginResult)).data as LoginResult;

+ 709 - 0
src/api/collect/InheritorContent.ts

@@ -0,0 +1,709 @@
+import { DataModel, transformArrayDataModel, transformDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import dayjs from 'dayjs';
+import { transformSomeToArray } from '../Utils';
+import { GetContentListItem } from '../CommonContent';
+
+export class CommonInfo<T extends DataModel> extends DataModel<T> {
+
+  constructor(classCreator?: (new () => T) | undefined, name: string = '基础信息') {
+    super(classCreator, name);
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      type: { clientSide: 'number', serverSide: 'number' },
+      keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      images: { clientSide: 'array', serverSide: 'array' },
+      expandInfo: { serverSide: 'undefined' },
+      region: { clientSide: 'number', serverSide: 'number' },
+      progress: { clientSide: 'number', serverSide: 'number' },
+      longitude: { clientSide: 'number', serverSide: 'number' },
+      latitude: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._beforeSolveClient = (data) => {
+      if (!data.contentId && data.id)
+        data.contentId = data.id;
+    }
+  }
+
+  contentId = null as number|null;
+  collectId = null as number|null;
+  
+  title = '' as string;
+  region = null as number|null;
+  image = null as string|null;
+  imageDesc = '' as string|null;
+  images = [] as string[];
+  audio = '' as string|null;
+  video = '' as string|null;
+  flag = [] as string[];
+  keywords = [] as string[];
+  tags = '' as string;
+  associationId = 0 as number;
+  pid = 0 as number;
+  content = '' as string|null;
+}
+
+export class IchInfo extends CommonInfo<IchInfo> {
+  constructor() {
+    super(IchInfo, "非遗项目信息");
+    this._convertTable = {
+      ...this._convertTable,
+      lonlat: { serverSide: 'undefined' },
+      batch: { clientSide: 'number', serverSide: 'string' },
+      typicalImages: [
+        { 
+          clientSide: 'object', 
+          clientSideChildDataModel: {
+            convertTable: {},
+          }, 
+          serverSide: 'string' 
+        },
+        {
+          clientSide: 'addDefaultValue',
+          clientSideParam: {
+            defaultValue: [],
+          }
+        },
+      ],
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+      self.lonlat = [ self.longitude, self.latitude ];
+      if (!self.intro && self.description)
+        self.intro = self.description;
+    };
+    this._afterSolveClient = (data) => {
+      data.longitude = this.lonlat[0];
+      data.latitude = this.lonlat[1];
+    };
+
+    const _superBeforeSolveClient = this._beforeSolveClient;
+    this._beforeSolveClient = (data) => {
+      _superBeforeSolveClient?.(data);
+      if (!this.expandInfo)
+        this.expandInfo = new IchExpandInfo();
+      this.expandInfo.batch = this.batch;
+      this.expandInfo.region = this.region;
+      this.expandInfo.image = this.image;
+      this.expandInfo.level = this.level!;
+      this.expandInfo.ichType = this.ichType!;
+      this.expandInfo.contentId = this.contentId!;
+      this.expandInfo.collectId = this.collectId!;
+
+    };
+  }
+
+  lonlat = [] as (number|string)[];
+  expandInfo : IchExpandInfo|null = new IchExpandInfo();
+
+  id = 0 as number;
+  modelId = 2;
+  mainBodyColumnId = 0 as number;
+  ztImage = '' as string|null;
+  intro = '' as string;
+  description = '' as string;
+  heritage = null as number|null;
+  level = null as number|null;
+  ichType = null as number|null;
+  batch = '' as string;
+  longitude = '' as string;
+  latitude = '' as string;
+  mapX = '' as string|null;
+  mapY = '' as string|null;
+  unit = '' as string;
+  address = '' as string|null;
+  declarationRegion = '' as string;
+  popularRegion = '' as string;
+  approveTime = '' as string;
+  typicalImages = [] as {
+    form: string,
+    mobile: string,
+    desc: string,
+    url: string,
+  }[];
+  thumbnail = '' as string;
+  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;
+
+}
+export class IchExpandInfo extends DataModel<IchExpandInfo> {
+  constructor() {
+    super(IchExpandInfo, "非遗项目信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      protectLevel: { clientSide: 'number', serverSide: 'string' },
+      id: { clientSide: 'number', serverSide: 'undefined' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+      if (key.endsWith('At')) {
+        return {
+          clientSide: 'date',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+    };
+    this._afterSolveClient = (data) => {
+    };
+  }
+
+  id = 0 as number;
+  modelId = 2;
+  userId = 0 as number;
+  reviewId = 0 as number;
+  originId = '' as string|null;
+  contentId = 0 as number;
+  name = '' as string;
+  level = 0 as number;
+  ichType = 0 as number;
+  protectLevel = null as number|null;
+  image = '' as string|null;
+  images = [] as string[];
+  otherNames = '' as string|null;
+  history = false;
+  existence = false;
+  folkCulture = '' as string|null;
+  culturalRelic = '' as string|null;
+  description = '' as string|null;
+  desc = '' as string;
+  mapX = '' as string|null;
+  mapY = '' as string|null;
+  declarationRegion = '' as string|null;
+  popularRegion = '' as string|null;
+  createdAt = '' as string;
+  updatedAt = '' as string;
+  deletedAt = '' as string|null;
+  progress = 0 as number;
+  comment = '' as string;
+  levelText = '' as string;
+  ichTypeText = '' as string;
+  protectLevelText = '' as string;
+  progressText = '' as string;
+}
+export class InheritorInfo extends CommonInfo<InheritorInfo> {
+  constructor() {
+    super(InheritorInfo, "传承人信息");
+    this._convertTable = {
+      ...this._convertTable,
+      gender: { clientSide: 'number', serverSide: 'string' },
+      level: { clientSide: 'number', serverSide: 'string' },
+      batch: { clientSide: 'number', serverSide: 'string' },
+      typicalImages: [
+        { 
+          clientSide: 'object', 
+          clientSideChildDataModel: {
+            convertTable: {},
+          }, 
+          serverSide: 'string' 
+        },
+        {
+          clientSide: 'addDefaultValue',
+          clientSideParam: {
+            defaultValue: [],
+          },
+          serverSide: 'original',
+        },
+      ],
+      works: {
+        clientSide: 'array',
+        clientSideChildDataModel: InheritorWorkInfo,
+        serverSide: 'array' 
+      },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+  }
+
+  expandInfo : InheritorExpandInfo|null = new InheritorExpandInfo();
+
+  id = 0 as number;
+  modelId = 7;
+  mainBodyColumnId = 0 as number;
+  alsoName = '' as string|null;
+  nation = '' as string;
+  dateBirth = '' as string;
+  deathBirth = '' as string|null;
+  unit = '' as string;
+  content = '' as string|null;
+  intro = '' as string;
+  prize = '' as string;
+  level = null as number|null;
+  gender = 0 as number;
+  batch = '' as string|null;
+  typicalImages = [] as string[];
+  progress = 0 as number;
+  contentId = 0 as number;
+  thumbnail = '' as string;
+  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;
+  progressText = '' as string;
+  works = [] as InheritorWorkInfo[];
+}
+export class InheritorExpandInfo extends DataModel<InheritorExpandInfo> {
+  constructor() {
+    super(InheritorExpandInfo, "非遗项目信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      gender: { clientSide: 'number', serverSide: 'string' },
+      level: { clientSide: 'number', serverSide: 'string' },
+      batch: { clientSide: 'number', serverSide: 'string' },
+      photosJson: [
+        { 
+          clientSide: 'object', 
+          clientSideChildDataModel: {
+            convertTable: {},
+          }, 
+          serverSide: 'string' 
+        },
+        {
+          clientSide: 'addDefaultValue',
+          clientSideParam: {
+            defaultValue: [],
+          }
+        },
+      ],
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+    };
+    this._afterSolveClient = (data) => {
+    };
+  }
+  modelId = 7;
+}
+export class InheritorWorkInfo extends DataModel<InheritorWorkInfo> {
+  constructor() {
+    super(InheritorWorkInfo, "传承人作品");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      collectionTime: { clientSide: 'dayjs', serverSide: 'string' },
+      type: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+      if (key.endsWith('At')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+    };
+    this._afterSolveClient = (data) => {
+    };
+  }
+
+  id = 0;
+  modelId = 16;
+  category = '';
+  feature = '';
+  otherName = '';
+  creator = '';
+  language = '';
+  overview = '';
+  ethnicGroup = '';
+  creationEra = '';
+  mainPerformer = '';
+  otherPerformers = '';
+  fullString = '';
+  tune = '';
+  development = '';
+  spread = '';
+  influence = '';
+  collector = '';
+  collectionTime = dayjs();
+  collectionLocation = '';
+}
+export class SeminarInfo extends CommonInfo<SeminarInfo> {
+  constructor() {
+    super(SeminarInfo, "传习所信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      ...this._convertTable,
+      lonlat: { serverSide: 'undefined' },
+      visit: { clientSide: 'number' },
+      ichSiteType: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+      self.lonlat = [ self.longitude, self.latitude ];
+    };
+    this._afterSolveClient = (data) => {
+      data.longitude = this.lonlat[0];
+      data.latitude = this.lonlat[1];
+    };
+  }
+
+  lonlat = [] as (number|string)[];
+  expandInfo : SeminarExpandInfo|null = new SeminarExpandInfo();
+
+  id = 0 as number;
+  modelId = 17;
+  mainBodyColumnId = 0 as number;
+  content = '' as string|null;
+  mapX = '' as string|null;
+  mapY = '' as string|null;
+  longitude = '' as string|null;
+  latitude = '' as string|null;
+  address = '' as string;
+
+  featuresType = null as number|null;
+  contact = '' as string;
+  ichSiteType = '' as string;
+  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;
+}
+export class SeminarExpandInfo extends DataModel<SeminarExpandInfo> {
+  constructor() {
+    super(SeminarExpandInfo, "非遗项目信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      lonlat: { serverSide: 'undefined' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+      self.lonlat = [ self.longitude, self.latitude ];
+    };
+    this._afterSolveClient = (data) => {
+      data.longitude = this.lonlat[0];
+      data.latitude = this.lonlat[1];
+    };
+  }
+  modelId = 17;
+  lonlat = [] as (number|string)[];
+}
+export class PlanInfo extends DataModel<PlanInfo> {
+  constructor() {
+    super(PlanInfo, "五年计划");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      progress: { clientSide: 'number', serverSide: 'undefined' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Text') || key.endsWith('_text')) {
+        return {
+          clientSide: 'string',
+          serverSide: 'undefined',
+        };
+      }
+    };
+    this._afterSolveServer = (self) => {
+    };
+    this._afterSolveClient = (data) => {
+    };
+  }
+
+  id = 0 as number;
+  ichId = 0 as number;
+  name = '' as string;
+  investment = 0 as number;
+  desc = '' as string;
+  target = '' as string;
+  unit = 0 as number;
+  department = 0 as number;
+  userId = 0 as number;
+  progress = 0 as number;
+  createdAt = '' as string;
+  updatedAt = '' as string;
+  ichName = '' as string;
+  progressText = '' as string;
+}
+export class InheritorAccountInfo extends DataModel<InheritorAccountInfo> {
+  constructor() {
+    super(InheritorAccountInfo, "传承人账号信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      username: { clientSide: 'string', clientSideRequired: true },
+      password: { clientSide: 'string', clientSideRequired: true },
+    };
+  }
+
+  id = 0 as number;
+  username = '' as string;
+  password = '' as string;
+  nickname = '' as string;
+}
+export class InheritorSubmitInfo extends DataModel<InheritorSubmitInfo> {
+  constructor() {
+    super(InheritorSubmitInfo, "传承人采集数据信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      progress: { clientSide: 'number', serverSide: 'undefined' },
+    };
+  }
+
+  id = 0 as number;
+  title = '' as string;
+  userId = 0 as number;
+  nickname = '' as string;
+  logintime = '' as string;
+  updatedAt = '' as string;
+  collectTotal = 0 as number;
+  progress = 0 as number;
+}
+
+export class InheritorContentApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async getBaseInfo<T extends DataModel>(id: number|undefined, newDataModel: new () => T, contentId?: number) {
+    return (await this.post('/ich/inheritor/baseInfo', '基础表信息', {
+      model_id: new newDataModel().modelId,
+      id,
+      content_id: contentId,
+    }, undefined, newDataModel)).data as T;
+  }
+  /**
+   * 项目五年计划
+   * @param ichId 项目ID:传承人只返回绑定项目的计划
+   * @param progress 审核进度:-1=不通过,0=待审核,1=已通过
+   * @returns 
+   */
+  async getPlanList(ichId: number, progress?: number) {
+    return transformArrayDataModel<PlanInfo>(
+      PlanInfo,
+      (await this.post('/ich/inheritor/plans', '获取计划列表', {
+        ich_id: ichId,
+        progress,
+      })).data2.data,
+      "data2"
+    );
+  }
+  async saveBaseInfo<T extends DataModel>(dataModel: T) {
+    return (await this.post('/ich/inheritor/saveBase', '基础内容表采集(非遗,传承人,传习所)', dataModel.toServerSide()));
+  }
+  async getExpandInfo<T extends DataModel>(id: number|undefined, newDataModel: new () => T) : Promise<T | null> {
+    return this.post('/ich/inheritor/expandInfo', '扩展表信息', {
+      model_id: new newDataModel().modelId,
+      id,
+    }, undefined).then((res) => {
+      if (!res.data2) 
+        return null;
+      return transformDataModel(newDataModel, res.data2) as T;
+    })
+  }
+  async saveExpandInfo<T extends DataModel>(dataModel: T) {
+    return (await this.post('/ich/inheritor/saveExpand', '扩展内容表采集(非遗,传承人,传习所)', dataModel.toServerSide()));
+  }
+  async saveWorkInfo(dataModel: InheritorWorkInfo) {
+    return (await this.post('/ich/inheritor/saveWork', '保存传承人作品信息', {
+      ...dataModel.toServerSide(),
+    }));
+  }
+  async savePlanInfo(dataModel: PlanInfo) {
+    return (await this.post('/ich/inheritor/savePlans', '保存项目五年计划', dataModel.toServerSide()));
+  }
+
+  async getCollectListInfo<T extends DataModel>(dataModel: new () => T, id: number) {
+    return this.post('/ich/inheritor/collectInfo', '获取采集记录详情', {
+      model_id: new dataModel().modelId,
+      id,
+    }, undefined).then((res) => {
+      return transformDataModel(dataModel, res.data2);
+    })
+  }
+  /**
+   * 获取采集列表
+   * @param data 
+   * @returns 
+   */
+  async getCollectList<T extends DataModel>(dataModel: new () => T, data: {
+    /**
+     * 采集类型
+     * * content 基础
+     * * ich 扩展
+     */
+    collectType: 'content'|'ich',
+    /**
+     * 提交用户ID
+     */
+    userId?: number,
+    /**
+     * 进度:-1=审核失败,0=待审核,1=审核通过
+     */
+    progress?: number,
+    /**
+     * 审核人用户ID
+     */
+    reviewId?: number,
+    /**
+     * 原基础表记录ID
+     */
+    contentId?: number,
+    page?: number,
+    pageSize?: number,
+  }) {
+    return this.post('/ich/inheritor/collectList', '获取采集列表', {
+      collect_type: data.collectType,
+      model_id: new dataModel().modelId,
+      user_id: data.userId,
+      progress: data.progress,
+      review_id: data.reviewId,
+      content_id: data.contentId,
+      page: data.page,
+      pageSize: data.pageSize,
+    }, undefined).then((res) => {
+      return {
+        data: transformArrayDataModel<T>(dataModel, transformSomeToArray(res.data2.data), 'data2'),
+        total: res.data2.total,
+      }
+    })
+  }
+
+  async getInheritorAccountInfo(contentId: number) {
+    return this.post('/ich/inheritor/getAccount', '获取传承人账号信息', {
+      content_id: contentId,
+    }, undefined).then((res) => {
+      const arr = transformSomeToArray(res.data2);
+      if (arr.length === 0)
+        return null;
+      return transformDataModel(InheritorAccountInfo, arr[0]);
+    })
+  }
+  async getInheritorSubmtList(modelId: number) {
+    return this.post('/ich/inheritor/list', '获取传承人采集数据列表', {
+      model_id: modelId
+    }, undefined).then((res) => {
+      return transformArrayDataModel<InheritorSubmitInfo>(InheritorSubmitInfo, transformSomeToArray(res.data2), 'data2');
+    })
+  }  
+
+  async getIchSeminarInfo(data: {
+    ichId?: number,
+    page?: number,
+    pageSize?: number,
+    keywords?: string,
+  }) {
+    return this.post('/ich/inheritor/sites', '获取传习所列表', {
+      ich_id: data.ichId,
+      page: data.page,
+      pageSize: data.pageSize,
+      keywords: data.keywords,
+    }, undefined).then((res) => {
+      return transformArrayDataModel<SeminarInfo>(SeminarInfo, transformSomeToArray(res.data2), 'data2');
+    })
+  }
+  async getIchWorksInfo(data: {
+    ichId: number,
+    page?: number,
+    pageSize?: number,
+  }) {
+    return this.post('/ich/inheritor/works', '获取项目作品列表', {
+      ich_id: data.ichId,
+      page: data.page,
+      pageSize: data.pageSize,
+    }, undefined).then((res) => {
+      return transformArrayDataModel<InheritorWorkInfo>(InheritorWorkInfo, res.data2.data, 'data2');
+    })
+  }
+  async getIchWorksDetail(id: number) {
+    return this.post('/ich/inheritor/info', '获取项目作品详情', {
+      id,
+      model_id: 16,
+    }, undefined).then((res) => {
+      return transformDataModel<InheritorWorkInfo>(InheritorWorkInfo, res.data2);
+    })
+  }
+
+  async getIchInfo(id: number|undefined) {
+    return await this.getBaseInfo(id, IchInfo);
+  }
+  async getInheritorInfo(id: number|undefined) {
+    return await this.getBaseInfo(id, InheritorInfo);
+  }
+  async getSeminarInfo(id: number|undefined) {
+    return await this.getBaseInfo(undefined, SeminarInfo, id);
+  }
+  async getIchExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, IchExpandInfo);
+  }
+  async getInheritorExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, InheritorExpandInfo);
+  }
+  async getSeminarExpandInfo(id: number|undefined) {
+    return await this.getExpandInfo(id, SeminarExpandInfo);
+  }
+}
+
+export default new InheritorContentApi();

+ 12 - 1
src/common/components/form/RichTextEditor.vue

@@ -1,8 +1,9 @@
 <template>
   <view class="d-flex flex-col">
     <view class="richtext-preview-box" @click="edit">
-      <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px" />
+      <Parse v-if="modelValue" :content="modelValue" contentStyle="max-height:400px;overflow:hidden;" />
       <Text v-else color="text.second">{{placeholder}}</Text>
+      <div class="richtext-preview-box-fade"></div>
     </view>
     <view class="d-flex flex-row gap-sss align-center mt-2">
       <Button icon="browse" text="预览" size="small" @click="preview" />
@@ -69,7 +70,17 @@ onPageShow(() => {
 
 <style lang="scss" scoped>
 .richtext-preview-box {
+  position: relative;
   flex: 1;
   min-height: 400rpx;
+
+  .richtext-preview-box-fade {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 100rpx;
+    background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
+  }
 }
 </style>

+ 23 - 0
src/common/upload/ImageUploadCo.ts

@@ -0,0 +1,23 @@
+import CommonContent from "@/api/CommonContent";
+import type { AntUploadRequestOption, UploadCoInterface } from "@/components/dynamicf/UploadImageFormItem";
+
+export function useImageSimpleUploadCo(additionData?: Record<string, any>) : UploadCoInterface {
+
+  return {
+    uploadRequest: (requestOption: AntUploadRequestOption) => {
+      CommonContent.uploadSmallFile(requestOption.file, 'image', 'file', additionData)
+        .then((res) => {
+          requestOption.onSuccess?.({
+            url: res.fullurl,
+            key: res.fullurl,
+          }, null);
+        }).catch((err) => {
+          requestOption.onError?.(err, {});
+        })
+    },
+    getUrlByUploadResponse: (response: unknown) => {
+      return (response as any).url as string;
+    },
+  }
+}
+

Diferenças do arquivo suprimidas por serem muito extensas
+ 12 - 0
src/components/basic/WxButton.vue


+ 2 - 0
src/components/dynamic/DynamicFormControl.vue

@@ -22,6 +22,7 @@
     :rules="item.rules"
     :disabled="disabled"
     :readonly="readonly"
+    :type="item.type === 'password' ? 'password' : 'text'"
     v-bind="{ 
       ...params,
       ...extraDefine?.itemProps || {},
@@ -304,6 +305,7 @@ const filedInternalTypes = [
   'text',
   'textarea',
   'text-tag',
+  'password',
 ]
 
 const props = defineProps({	

+ 1 - 0
src/components/form/Picker.vue

@@ -59,6 +59,7 @@ const emit = defineEmits([ 'update:value', 'selectTextChange' ]);
 const props = withDefaults(defineProps<PickerProps>(), {
   pickerHeight: 300,
   pickerWidth: 750,
+  columns: () => [],
 });
 
 const loaded = ref(false);

+ 43 - 0
src/pages.json

@@ -237,6 +237,49 @@
           "path": "inheritor",
           "style": {
             "navigationBarTitleText": "非遗数字化资源",
+            "enablePullDownRefresh": false,
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "user/change-password",
+          "style": {
+            "navigationBarTitleText": "修改密码",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/ich",
+          "style": {
+            "navigationBarTitleText": "非遗项目",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/inheritor",
+          "style": {
+            "navigationBarTitleText": "传承人",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/seminar",
+          "style": {
+            "navigationBarTitleText": "传习所",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "works",
+          "style": {
+            "navigationBarTitleText": "作品",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/works",
+          "style": {
+            "navigationBarTitleText": "作品",
             "enablePullDownRefresh": false
           }
         }

+ 137 - 0
src/pages/article/common/CommonListBlock.vue

@@ -0,0 +1,137 @@
+<template>
+  <!-- 通用列表 -->
+  <view class="position-relative d-flex flex-row flex-wrap justify-between align-stretch mt-3">
+    <view
+      v-for="(item, i) in listLoader.list.value"
+      :key="item.id"
+      :class="[
+        'position-relative d-flex flex-grow-1',
+        itemType.endsWith('-2') ? 'width-1-2' : 'w-100'
+      ]"
+    >
+      <Box2LineLargeImageUserShadow 
+        v-if="itemType.startsWith('image-large')"
+        class="w-100"
+        titleColor="title-text"
+        :classNames="getItemClass(i)"
+        :image="getImage(item)"
+        :titleBox="item.titleBox"
+        :titlePrefix="item.titlePrefix"
+        :title="getTitle(item)"
+        :desc="item.desc"
+        :tags="item.bottomTags"
+        :badge="item.badge"
+        @click="goDetails(item, item.id)"
+      />
+      <Box2LineImageRightShadow 
+        v-else-if="itemType.startsWith('article-common')"
+        class="w-100"
+        titleColor="title-text"
+        :titleBox="item.titleBox"
+        :titlePrefix="item.titlePrefix"
+        :classNames="getItemClass(i)"
+        :image="getImage(item)"
+        :title="getTitle(item)"
+        :desc="item.desc"
+        :tags="item.bottomTags"
+        :badge="item.badge"
+        :wideImage="true"
+        @click="goDetails(item, item.id)"
+      />
+      <Box2LineImageRightShadow 
+        v-else-if="itemType.startsWith('article-character')"
+        class="w-100"
+        :classNames="getItemClass(i)"
+        :image="getImage(item)"
+        titleColor="title-text"
+        :title="getTitle(item)"
+        :titlePrefix="item.titlePrefix"
+        :titleBox="item.titleBox"
+        :tags="item.bottomTags || item.keywords"
+        :desc="item.desc"
+        :badge="item.badge"
+        @click="goDetails(item, item.id)"
+      />
+      <Box2LineImageRightShadow 
+        v-else-if="itemType.startsWith('simple-text')"
+        class="w-100"
+        :classNames="getItemClass(i)"
+        titleColor="title-text"
+        :border="false"
+        :showImage="false"
+        :title="getTitle(item)"
+        :titlePrefix="item.titlePrefix"
+        :titleBox="item.titleBox"
+        :tags="item.bottomTags || item.keywords"
+        :desc="item.desc"
+        :badge="item.badge"
+        @click="goDetails(item, item.id)"
+      />
+      <Grid4Item 
+        v-else-if="itemType.startsWith('image-small')"
+        :title="getTitle(item)"
+        :image="item.image"
+        :classNames="itemType.endsWith('-2') ? 'half' : 'full'"
+        @click="goDetails(item, item.id)"
+      />
+      <Box2LineRightShadow
+        v-else
+        :key="i" 
+        :title="getTitle(item)"
+        :titleBox="item.titleBox"
+        :desc="item.desc"
+        :tags="(item.bottomTags as string[])"
+        @click="goDetails(item, item.id)"
+      />
+      <slot name="itemRight" :item="item" :index="i" />
+    </view>
+    <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" class="width-1-2" />
+  </view>
+  <Empty v-if="listLoader.list.value.length == 0 && listLoader.loadStatus.value !== 'loading'" :description="emptyText" />
+  <SimplePageListLoader v-else :loader="listLoader" />
+</template>
+
+<script setup lang="ts">
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import Box2LineLargeImageUserShadow from '@/pages/parts/Box2LineLargeImageUserShadow.vue';
+import Box2LineImageRightShadow from '@/pages/parts/Box2LineImageRightShadow.vue';
+import Empty from '@/components/feedback/Empty.vue';
+import Grid4Item from '@/pages/parts/Grid4Item.vue';
+import Box2LineRightShadow from '@/pages/parts/Box2LineRightShadow.vue';
+import type { CommonListPageItemType } from './CommonListPage';
+import type { CommonListItem } from './CommonListPage.vue';
+
+function getImage(item: any) {
+  return item.thumbnail || item.image
+}
+function getItemClass(index: number) {
+  return props.itemType.endsWith('-2') ? (index % 2 != 0 ? 'ml-1' : 'mr-1') : ''
+}
+function getTitle(item: CommonListItem) {
+  return item[props.titleKey]
+}
+
+const emit = defineEmits([ 'goDetails' ])
+const props = withDefaults(defineProps<{
+  pageSize: number,
+  load: (page: number, pageSize: number) => Promise<{ list: CommonListItem[], total: number }>,
+  itemType?: CommonListPageItemType,
+  emptyText?: string,
+  titleKey?: string,
+}>(), {
+  titleKey: 'title',
+  emptyText: '暂无数据',
+  itemType: 'article-common',
+})
+
+const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize) => {
+  return await props.load(page, pageSize)
+});
+function goDetails(item: CommonListItem, id: number) {
+  emit('goDetails', item, id)
+}
+</script>
+
+<style lang="scss">
+</style>

+ 386 - 0
src/pages/collect/forms/form.vue

@@ -0,0 +1,386 @@
+<template>
+  <!-- 表单 -->
+  <FlexCol gap="gap.xl" padding="space.lg">
+    <Alert
+      v-if="(formModel as any).progress === -1 && !isReviewer && !isAdmin" 
+      message="提示:您的信息已被审核退回,请根据审核建议修改后重新提交。"
+      :description="(formModel as any).comment"
+      type="warning"
+      showIcon
+    />
+    <Result 
+      v-if="loadError"
+      :title="loadError"
+      type="error"
+    />
+    <template v-else>
+      <DynamicForm
+        ref="formBase"
+        :model="(formModel as any)" 
+        :options="finalFormOptions"
+      />
+      <FlexCol gap="gap.md">
+        <Alert type="info" message="提示:上传文件时请勿离开页面防止上传失败,离开之前请保存您的修改以防丢失。
+        如果未完成编辑,可以先点击“保存”按钮保存修改,完成后再点击“提交”审核。您可以在历史版本中查看之前的修改。" />
+        
+        <Button type="primary" block :loading="loading" @click="handleSubmitBase(true)">
+          提交
+        </Button>
+        <Button block @click="handleSubmitBase(false)">
+          保存
+        </Button>
+        <Button block @click="handleShowHistory">
+          历史版本
+        </Button>
+      </FlexCol>
+    </template>
+
+    <Popup
+      v-model:show="showHistory"
+      position="bottom"
+      size="80vh"
+      title="历史版本"
+      closeable
+    >
+      <FlexCol v-if="showHistoryModel">
+        <NavBar 
+          leftButton="arrow-left-bold" 
+          :title="`您正在查看 ${showHistoryModel.desc} 保存的版本`"
+          @leftButtonPressed="showHistory = false" 
+        />
+        <ActivityIndicator v-if="showHistoryLoading" />
+        <DynamicForm
+          v-else
+          :model="(showHistoryModel as any)" 
+          :options="{
+            ...formOptions,
+            disabled: true,
+          }"
+        />
+      </FlexCol>
+      <CommonListBlock
+        v-else
+        :load="(page: number, pageSize: number) => loadHistoryData(page, pageSize)"
+        :pageSize="10"
+        titleKey="_title"
+      >
+        <template #itemRight="{ item }">
+          <Button type="text" @click.stop="handleShowHistory(item)">查看</Button>
+        </template>
+      </CommonListBlock>
+    </Popup>
+  </FlexCol>
+</template>
+
+<script setup lang="ts" generic="T extends DataModel, U extends DataModel">
+import { onMounted, ref, toRefs, type PropType, h, computed } from 'vue';
+import { useAuthStore } from '@/store/auth';
+import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
+import { alert, confirm, toast } from '@/components/utils/DialogAction';
+import { ArrowLeftOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
+import { formatError, waitTimeOut } from '@imengyu/imengyu-utils';
+import type { DataModel } from '@imengyu/js-request-transform';
+import InheritorContent, { InheritorWorkInfo } from '@/api/collect/InheritorContent';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Alert from '@/components/feedback/Alert.vue';
+import Popup from '@/components/dialog/Popup.vue';
+import Button from '@/components/basic/Button.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import type { FormInstance } from '@/components/form/Form.vue';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { back } from '@/components/utils/PageAction';
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import NavBar from '@/components/nav/NavBar.vue';
+import CommonListBlock from '@/pages/article/common/CommonListBlock.vue';
+import type { CommonListItem } from '@/pages/article/common/CommonListPage.vue';
+import Result from '@/components/feedback/Result.vue';
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: '非遗数字化资源信息校对'
+  },
+  basicTabText: {
+    type: String,
+    default: '基础信息'
+  },
+  formModel: {
+    type: Object as PropType<T>,
+    required: true
+  },
+  formOptions: {
+    type: Object as PropType<IDynamicFormOptions>,
+    required: true
+  },
+  model: {
+    type: Function as unknown as PropType<new () => DataModel>,
+    required: true
+  },
+  load: {
+    type: Function as PropType<(id: number|undefined) => Promise<void>>,
+    default: () => Promise.resolve()
+  },
+  save: {
+    type: Function as PropType<(model: T) => Promise<DataModel>>,
+    default: (a: any) => Promise.resolve(a)
+  },
+  saveExtend: {
+    type: Function as PropType<(model: U) => Promise<DataModel>>,
+    default: (a: any) => Promise.resolve(a)
+  },
+  pushExamine: {
+    type: Boolean,
+    default: false
+  },
+})
+
+const { formModel, formOptions, load } = toRefs(props);
+const formBase = ref<IDynamicFormRef>();
+
+const { querys } = useLoadQuerys({
+  id: 0,
+  readonly: false,
+});
+
+const authStore = useAuthStore();
+const loading = ref(false);
+const loadingData = ref(false);
+const loadError = ref('');
+const readonly = ref(false);
+const showHistory = ref(false);
+const showHistoryLoading = ref(false);
+const showHistoryModel = ref<any>(null);
+const isAdmin = computed(() => Boolean(authStore.isCollectAdmin));
+const isReviewer = computed(() => Boolean(authStore.isCollectReviewer));
+const finalFormOptions = computed(() => {
+  
+  const options: IDynamicFormOptions = {
+    ...formOptions.value,
+    formAdditionaProps: {
+      labelFlex: 4,
+      inputFlex: 8,
+    },
+    formItems: [
+      ...formOptions.value.formItems,
+      ...(props.pushExamine ? [
+        {
+          type: 'flat-group', label: '审核', name: 'ichInfo',
+          childrenColProps: { span: 24 },
+          children: [
+            { 
+              label: '已排版完成', name: 'typesetting', type: 'check-box-int', 
+              show: { callback: (_: any, model: any) => (isAdmin.value) },
+              additionalProps: {
+                text: '确认',
+              },
+              formProps: {
+                extraMessage: '编辑员专用,如果已经排版,请打勾'
+              }
+            },
+            { 
+              label: '审核人员', name: 'text1', type: 'static-text', 
+              show: { callback: (_: any, model: any) => !(isAdmin.value || isReviewer.value) },
+              additionalProps: {
+                text: '黄念旭,李向群,卢志明',
+                style: { color: '#999', }
+              }
+            },
+            { 
+              label: '填报人', name: 'text3', type: 'static-text', 
+              show: { callback: (_: any, model: any) => (!isAdmin.value && !isReviewer.value) },
+              additionalProps: {
+                text: authStore.userInfo?.nickname,
+              }
+            },
+            /* { 
+              label: '填报人', name: 'text3', type: 'static-text', 
+              show: { callback: (_: any, model: any) => (isAdmin.value || isReviewer.value) },
+              additionalProps: {
+                text: authStore.userInfo?.nickname,
+              }
+            }, */
+            { 
+              label: '初审状态', name: 'progress', type: 'select', 
+              show: { callback: (_: any, model: any) => (isAdmin.value && !isReviewer.value) },
+              additionalProps: {
+                columns: [[
+                  { text: '保存未审核', value: -2, disabled: true },
+                  { text: '审核退回', value: -1 },
+                  { text: '暂未审核', value: 0 },
+                  { text: '初审通过', value: 1 },
+                  { text: '专家审核通过', value: 2, disabled: true },
+                ]]
+              }
+            },
+            { 
+              label: '审核状态', name: 'progress', type: 'select', 
+              show: { callback: (_: any, model: any) => (isReviewer.value) },
+              additionalProps: {
+                columns: [[
+                  { text: '未审核', value: -2, disabled: true },
+                  { text: '审核退回', value: -1 },
+                  { text: '未审核', value: 0, disabled: true },
+                  { text: '未审核', value: 1, disabled: true },
+                  { text: '通过审核', value: 2 },
+                ]],
+              }
+            },
+            { 
+              label: '审核状态', name: 'progress', type: 'select', 
+              show: { callback: (_: any, model: any) => !(isAdmin.value || isReviewer.value) },
+              disabled: true,
+              additionalProps: {
+                columns: [[
+                  { text: '保存未审核', value: -2 },
+                  { text: '审核退回', value: -1 },
+                  { text: '暂未审核', value: 0 },
+                  { text: '初审通过', value: 1 },
+                  { text: '专家审核通过', value: 2 },
+                ]],
+              }
+            },
+            { 
+              label: '审核意见', name: 'comment', type: 'textarea',
+              disabled: { callback: (_: any, model: any) => !isAdmin.value },
+              additionalProps: {
+                placeholder: { callback: (_: any, model: any) => (isAdmin.value || isReviewer.value) ? '若审核不通过,请输入审核意见' : '暂无审核意见' },
+              }
+            },
+            {
+              label: '审核签名', name: 'sign', type: 'sign',
+              show: { callback: (_: any, model: any) => (isReviewer.value) },
+              additionalProps: {
+                uploadCo: useImageSimpleUploadCo({}),
+              }
+            },
+          ]
+        }
+      ] : [])
+    ],
+    formRules: {
+      ...formOptions.value.formRules,
+      sign: [{ required: true, message: '请审核签名' }],
+    },
+    disabled: readonly.value,
+  }
+  return options;
+});
+
+async function handleSubmitBase(valid: boolean) {
+  loading.value = true;
+
+  if (valid) {
+    if (!isAdmin.value && !await new Promise((resolve, reject) => {
+      confirm({
+        title: '提交提示',
+        content: '是否提交信息审核?填写完整信息后才可提交审核。如果需要离开,可先保存修改下次接着编辑。',
+      }).then((res) => resolve(res))
+    })) {
+      loading.value = false;
+      return;
+    }
+  }
+
+  const ref = (formBase.value?.getFormRef() as FormInstance);
+  if (valid) {
+    try {
+      await ref.validate();
+    } catch (e) {
+      toast('请填写完整信息');
+      loading.value = false;
+      return;
+    }
+  }
+  try {
+
+    let result = null;
+    const data = await props.save(formModel.value);
+    data.progress = valid ? 0 : -2;
+    if (formModel.value instanceof InheritorWorkInfo)
+      result = await InheritorContent.saveWorkInfo(data as InheritorWorkInfo);
+    else
+      result = await InheritorContent.saveBaseInfo(data);
+    alert({
+      title: '提交成功',
+      content: result.message,
+    });
+  } catch (error) {
+    alert({
+      title: '提交失败',
+      content: '' + error,
+    });
+  } finally {
+    loading.value = false;
+  }
+}
+async function loadData() {
+  loadingData.value = true;
+  readonly.value = querys.value.readonly;
+  try {
+    loadError.value = '';
+    await load.value(querys.value.id ? Number(querys.value.id) : undefined);
+  } catch (error) {
+    console.log(error);
+    loadError.value = formatError(error);
+    toast('加载失败 ' + error);
+  } finally {
+    loadingData.value = false;
+  }
+}
+async function loadHistoryData(page: number, pageSize: number) {
+  const contentId = Number(querys.value.id || formModel.value.contentId)
+  if (isNaN(contentId))
+    return {
+      page,
+      total: 0,
+      list: []
+    };
+  const res = (await InheritorContent.getCollectList(props.model, {
+    contentId,
+    collectType: 'content',
+    userId: authStore.userInfo?.id,
+    page,
+    pageSize
+  }))
+  return {
+    page,
+    total: res.total,
+    list: res.data.map((p) => {
+      p._title = p.nickname ? `提交人:${p.nickname}` : p.title;
+      p.desc = `提交时间:${p.updatedAt}`;
+      return p as unknown as CommonListItem;
+    }),
+  };
+}
+
+async function handleShowHistory(item: any) {
+  showHistoryLoading.value = true;
+  showHistory.value = true;
+  await waitTimeOut(100);
+  showHistoryModel.value = item;
+  showHistoryLoading.value = false;
+}
+
+function handleBack() {
+  confirm({
+    title: '确定返回吗?',
+    content: '返回后将丢失当未提交的信息,若有修改请先提交哦!',
+  }).then((res) => {
+    if (res) {
+      back();
+    }
+  });
+}
+
+onMounted(async () => {
+  await loadData();
+})
+
+defineExpose({
+  getFormRef() {
+    return formBase.value;
+  },
+})
+</script>
+

+ 140 - 0
src/pages/collect/forms/ich.vue

@@ -0,0 +1,140 @@
+<template>
+  <!-- 非遗表单 -->
+  <Form 
+    ref="formRef"
+    :formModel="formModel"
+    :formOptions="formOptions"
+    :load="loadData"
+    :model="IchInfo"
+    pushExamine
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, type Ref } from 'vue';
+import Form from './form.vue';
+import InheritorContent, { IchInfo } from '@/api/collect/InheritorContent';
+import CommonContent from '@/api/CommonContent';
+import type { IDynamicFormOptions } from '@/components/dynamic';
+
+const formModel = ref(new IchInfo()) as Ref<IchInfo>;
+const formOptions = ref<IDynamicFormOptions>({
+  formItems: [
+    {
+      type: 'flat-group', label: '非遗基础档案', name: 'ichInfo',
+      childrenColProps: { span: 24 },
+      children: [
+        { 
+          label: '非遗项目名称', name: 'title', type: 'text',
+          additionalProps: {
+            placeholder: '请输入标题',
+          },
+        },
+        { 
+          label: '级别', name: 'level', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择级别',
+            loadData: async () => (await CommonContent.getCategoryList(2)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },  
+        },
+        { 
+          label: '非遗分类', name: 'ichType', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择非遗类型',
+            loadData: async () => (await CommonContent.getCategoryList(4)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },
+        { 
+          label: '批次', name: 'batch', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择批次',
+            loadData: async () => (await CommonContent.getCategoryList(289)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },
+        { label: '申报区域', name: 'regionText', type: 'text', additionalProps: { placeholder: '请输入申报地区' } },
+        
+        { label: '非遗项目简介', name: 'intro', type: 'richtext', additionalProps: { placeholder: '请输入简介' } },
+        { 
+          label: '传承谱系', name: 'pedigree', type: 'richtext', 
+          additionalProps: { placeholder: '请输入传承谱系' },
+          formProps: {
+            extraMessage:  `请按传承脉络顺序填写:
+1、传承人:姓名(附简短介绍,如技艺特长/代表成就)
+2、传承代数:标注第几代(示例:第五代传人)
+3、师从关系:明确师承对象(如:师从第四代传人XXX)
+注:信息需真实可考,传承脉络清晰有序`
+          },
+        },
+        //{ label: '非遗详细描述', name: 'description', type: 'richtext', additionalProps: { placeholder: '请输入项目描述' } },
+        //{ label: '传承值', name: 'heritage', type: 'text', additionalProps: { placeholder: '请输入传承值' } },
+        
+        { 
+          label: '保护单位地址', name: 'address', type: 'address-sercher', 
+          additionalProps: { placeholder: '请输入地址' },
+          /* TODO: additionalEvents: {
+            choosedAddress: (address: AddressItem) => {
+              ((formRef.value?.getFormRef() as IDynamicFormRef).getFormItemControlRef('lonlat') as any).moveTo([
+                address.lng, address.lat
+              ], 20)
+            },
+          } */
+        },
+        { 
+          label: '地图坐标', name: 'lonlat', type: 'map-pick-point',
+          formProps: {
+            extraMessage: '输入模糊地址后可以点击搜索跳转到指定位置,如果地图位置不正确,可以手动拖拽调整位置',
+          },
+        },
+        { label: '保护单位(多个保护单位请用逗号隔开)', name: 'unit', type: 'text', additionalProps: { placeholder: '请输入保护单位' } },
+        { 
+          label: '非遗项目相关图片', name: 'images', type: 'uploader',
+          //hidden: { callback: (_, model) => (model as IchInfo).type !== 4 },
+          formProps: {
+            extraMessage: '建议分辨率:1920*1080以上',
+          },
+          additionalProps: {
+            placeholder: '请上传图片',
+            maxCount: 100,
+            name: 'file',
+            accept: 'image/*',
+          },
+        },
+        { 
+          label: '相关视频', name: 'video', type: 'uploader',
+          //hidden: { callback: (_, model) => (model as IchInfo).type !== 3 },
+          additionalProps: {
+            placeholder: '请上传视频',
+            accept: 'video/*',
+            name: 'file',
+          }
+        },
+        { 
+          label: '其他附件', name: 'annex', type: 'uploader',
+          //hidden: { callback: (_, model) => (model as IchInfo).type !== 3 },
+          formProps: {
+            extraMessage: '可以上传多个视频文件',
+          },
+          additionalProps: {
+            placeholder: '请上传视频',
+            name: 'file',
+          },  
+        },
+      ]
+    },
+  ],
+  formRules: {
+    title: [{ required: true, message: '请输入标题' }],
+    region: [{ required: true, message: '请选择地区' }],
+    type: [{ required: true, message: '请选择类型' }],
+    image: [{ required: true, message: '请上传图片' }],
+    level: [{ required: true, message: '请选择级别' }],
+    ichType: [{ required: true, message: '请选择非遗类型' }],
+    batch: [{ required: true, message: '请输入批次' }],
+  }
+});
+
+async function loadData(id: number|undefined) {
+  formModel.value = await InheritorContent.getIchInfo(id);
+}
+
+</script>

+ 140 - 0
src/pages/collect/forms/inheritor.vue

@@ -0,0 +1,140 @@
+<template>
+  <!-- 传承人基础表单 -->
+  <Form 
+    :formModel="formModel"
+    :formOptions="formOptions"
+    :load="loadData"
+    :model="InheritorInfo"
+    pushExamine
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, type Ref } from 'vue';
+import Form from './form.vue';
+import InheritorContent, { InheritorInfo } from '@/api/collect/InheritorContent';
+import CommonContent from '@/api/CommonContent';
+import type { IDynamicFormOptions } from '@/components/dynamic';
+
+const formModel = ref(new InheritorInfo()) as Ref<InheritorInfo>;
+const formOptions = ref<IDynamicFormOptions>({
+  formItems: [
+    {
+      type: 'flat-group', label: '传承人基础档案', name: 'ichInfo',
+      childrenColProps: { span: 24 },
+      children: [
+        { 
+          label: '姓名', name: 'title', type: 'text',
+          additionalProps: { placeholder: '请输入标题' },
+        },
+        { 
+          label: '性别', name: 'gender', type: 'select',
+          additionalProps: {
+            placeholder: '请选择性别',
+            columns: [[
+              { text: '女', value: 0 },
+              { text: '男', value: 1 },
+              { text: '未知', value: 2 }
+            ]],
+          },
+        },
+        { 
+          label: '传承人等级', name: 'level', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择传承人等级',
+            loadData: async () => (await CommonContent.getCategoryList(2)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },
+        { 
+          label: '传承人批次', name: 'batch', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择传承人批次',
+            loadData: async () => (await CommonContent.getCategoryList(289)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },
+        //{ label: '别称', name: 'alsoName', type: 'text', additionalProps: { placeholder: '请输入别称' } },
+        //{ label: '时代', name: 'age', type: 'text', additionalProps: { placeholder: '请输入时代' } },
+        { label: '联系电话', name: 'mobile', type: 'text', additionalProps: { placeholder: '请输入联系电话' } },
+        { label: '籍贯', name: 'birthplace', type: 'text', additionalProps: { placeholder: '请输入籍贯' } },
+        { label: '民族', name: 'nation', type: 'text', additionalProps: { placeholder: '请输入民族' } },
+        { label: '出生日期', name: 'dateBirth', type: 'text', additionalProps: { placeholder: '请选择出生日期' } },
+        //{ label: '逝世日期', name: 'deathBirth', type: 'text', additionalProps: { placeholder: '请选择逝世日期' } },
+        { label: '保护单位', name: 'unit', type: 'text', additionalProps: { placeholder: '请输入单位' } },
+        { label: '传承人简介', name: 'intro', type: 'richtext', additionalProps: { placeholder: '请输入简介' } },
+        //{ label: '详情', name: 'content', type: 'richtext', additionalProps: { placeholder: '请输入详情' } },
+        { label: '奖项/成就', name: 'prize', type: 'richtext', additionalProps: { placeholder: '请输入奖项-成就' } },
+        { label: '传承人谱系', name: 'pedigree', type: 'richtext', additionalProps: { placeholder: '请输入传承谱系' } },
+        { 
+          label: '传承人照片', name: 'images', type: 'uploader',
+          //hidden: { callback: (_, model) => (model as IchInfo).type !== 4 },
+          formProps: {
+            extraMessage: '建议分辨率:1920*1080以上,请上传传承人证件照、工作照、生活照、实践活动照',
+          },
+          additionalProps: {
+            placeholder: '请上传图片',
+            maxCount: 100,
+            name: 'file',
+            accept: 'image/*',
+            // beforeUpload: useBeforeUploadImageChecker(),
+            // uploadCo: useAliOssUploadCo('inheritor/images'),
+          },
+        },
+        /* { 
+          type: 'array-object', label: '传承人照片(建议分辨率:1920*1080以上,请上传传承人证件照、工作照、生活照、实践活动照)', 
+          name: 'typicalImages',
+          formProps: {
+            center: false,
+          },
+          additionalProps: {
+            direction: 'horizontal'
+          },
+          newChildrenObject: (arrayNow) => ({
+            desc: `代表性图片${arrayNow.length+1}`,
+            url: '',
+            from: '',
+            mobile: '',
+          }),
+          children: [
+            { type: 'text', label: '照片来源', name: 'from', additionalProps: { placeholder: '请输入来源' } },
+            //{ type: 'text', label: '联系方式', name: 'mobile', additionalProps: { placeholder: '请输入联系方式' } },
+            { type: 'text', label: '照片说明', name: 'desc', additionalProps: { placeholder: '请输入说明' } },
+            { 
+              label: '照片', name: 'url', type: 'uploader',
+              additionalProps: {
+                name: 'file',
+                placeholder: '请上传图片',
+                uploadCo: useImageSimpleUploadCo(),
+              } as UploadImageFormItemProps,
+            },
+          ]
+        }, */
+        { 
+          label: '传承人相关视频', name: 'video', type: 'uploader',
+          //hidden: { callback: (_, model) => (model as InheritorInfo).type !== 3 },
+          additionalProps: {
+            placeholder: '请上传视频',
+            name: 'file',
+            accept: 'video/*',
+            // beforeUpload: useBeforeUploadVideoChecker(),
+            // uploadCo: useAliOssUploadCo('inheritor/video'),
+          },  
+        },
+      ]
+    },
+  ],
+  formRules: {
+    title: [{ required: true, message: '请输入标题' }],
+    region: [{ required: true, message: '请选 择地区' }],
+    type: [{ required: true, message: '请选择类型' }],
+    image: [{ required: true, message: '请上传图片' }],
+    gender: [{ required: true, message: '请选择性别' }],
+    level: [{ required: true, message: '请选择级别' }],
+    batch: [{ required: true, message: '请输入批次' }],
+    ichType: [{ required: true, message: '请选择非遗类型' }],
+  }
+});
+
+async function loadData(id: number|undefined) {
+  formModel.value = await InheritorContent.getInheritorInfo(id);
+}
+</script>

+ 137 - 0
src/pages/collect/forms/seminar.vue

@@ -0,0 +1,137 @@
+<template>
+  <!-- 传习所基础表单-->
+  <Form 
+    ref="formRef"
+    :formModel="formModel"
+    :formOptions="formOptions"
+    :load="loadData"
+    :model="SeminarInfo"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, type Ref } from 'vue';
+import Form from './form.vue';
+import InheritorContent, { SeminarInfo } from '@/api/collect/InheritorContent';
+import CommonContent from '@/api/CommonContent';
+import type { IDynamicFormOptions } from '@/components/dynamic';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useAuthStore } from '@/store/auth';
+
+const authStore = useAuthStore();
+
+const formModel = ref(new SeminarInfo()) as Ref<SeminarInfo>;
+const formOptions = ref<IDynamicFormOptions>({
+  formItems: [
+    {
+      type: 'flat-group', label: '传习所/保护单位信息', name: 'seminarInfo',
+      childrenColProps: { span: 24 },
+      children: [
+        { 
+          label: '传习所名称', name: 'title', type: 'text',
+          additionalProps: { placeholder: '请输入标题' },
+        },
+        { 
+          label: '批次', name: 'batch', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择批次',
+            loadData: async () => (await CommonContent.getCategoryList(289)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },
+        { 
+          label: '传习所级别', name: 'level', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择传习所级别',
+            loadData: async () => (await CommonContent.getCategoryList(2)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          },
+        },{ 
+          label: '图片', name: 'image', type: 'uploader',
+          additionalProps: {
+            placeholder: '请上传图片',
+            name: 'file',
+            accept: 'image/*',
+            // beforeUpload: useBeforeUploadImageChecker(),
+            // uploadCo: useAliOssUploadCo('seminar/images')
+          },
+        },
+        { label: '传习所介绍', name: 'content', type: 'richtext', additionalProps: { placeholder: '请输入内容' } },
+        
+        { label: '传习所地址', name: 'address', type: 'address-sercher', 
+          additionalProps: { placeholder: '请输入地址' },
+          /* additionalEvents: {
+            choosedAddress: (address: AddressItem) => {
+              ((formRef.value?.getFormRef() as IDynamicFormRef).getFormItemControlRef('lonlat') as any).moveTo([
+                address.lng, address.lat
+              ], 20)
+            },
+          } */
+        },
+        { label: '地图坐标', name: 'lonlat', type: 'map-pick-point' },
+        { label: '联系人', name: 'contact', type: 'text', additionalProps: { placeholder: '请输入联系人' } },
+        { label: '联系电话', name: 'mobile', type: 'text', additionalProps: { placeholder: '请输入联系电话' } },
+        { 
+          label: '传习所/保护单位类型', name: 'ichSiteType', type: 'select',
+          additionalProps: {
+            placeholder: '请选择非遗单位类型',
+            columns: [[
+              { text: '传习所', value: 1 },
+              { text: '保护单位', value: 2 }
+            ]],
+          },
+        },
+        { 
+          label: '是否对游客开放', name: 'visit', type: 'select',
+          additionalProps: {
+            placeholder: '请选择是否对游客开放',
+            columns: [[
+              { text: '否', value: 0 },
+              { text: '是', value: 1 }
+            ]],
+          },
+        },
+        { 
+          label: '审核人员', name: 'text1', type: 'static-text', 
+          additionalProps: {
+            text: '黄念旭,李向群,卢志明',
+            style: { color: '#999', }
+          }
+        },
+        { 
+          label: '审核状态', name: 'text2', type: 'static-text', 
+          additionalProps: {
+            text: '暂未审核',
+            style: { color: '#999', }
+          }
+        },
+        { 
+          label: '填报人', name: 'text3', type: 'static-text', 
+          additionalProps: {
+            text: authStore.userInfo?.nickname,
+          }
+        },
+      ]
+    },
+  ],
+  formRules: {
+    'expandInfo.protectLevel': [{ required: true, message: '请选择保护级别' }],
+    title: [{ required: true, message: '请输入标题' }],
+    region: [{ required: true, message: '请选择地区' }],
+    type: [{ required: true, message: '请选择类型' }],
+    image: [{ required: true, message: '请上传图片' }],
+    level: [{ required: true, message: '请选择级别' }],
+    ichType: [{ required: true, message: '请选择非遗类型' }],
+    batch: [{ required: true, message: '请输入批次' }]
+  }
+});
+
+const { querys } = useLoadQuerys({
+  ichId: 0,
+})
+
+async function loadData(id: number|undefined) {
+  formModel.value = id === undefined || id > 0 ? await InheritorContent.getSeminarInfo(id) : new SeminarInfo();
+  if (id === -1)
+    formModel.value.associationId = querys.value.ichId ?? 0;
+}
+
+</script>

+ 148 - 0
src/pages/collect/forms/works.vue

@@ -0,0 +1,148 @@
+<template>
+  <!-- 传承人作品采集 -->
+  <Form 
+    :formModel="formModel"
+    :formOptions="formOptions"
+    :load="loadData"
+    :model="InheritorWorkInfo"
+    basicTabText="作品/产品信息"
+    :save="handleSave"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, type Ref } from 'vue';
+import Form from './form.vue';
+import InheritorContent, { InheritorWorkInfo } from '@/api/collect/InheritorContent';
+import CommonContent from '@/api/CommonContent';
+import type { IDynamicFormOptions } from '@/components/dynamic';
+import { useAuthStore } from '@/store/auth';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+
+const authStore = useAuthStore();
+const formModel = ref(new InheritorWorkInfo()) as Ref<InheritorWorkInfo>;
+const formOptions = ref<IDynamicFormOptions>({
+  formItems: [
+      {
+        type: 'flat-group', label: '作品/产品信息', name: 'baseInfo',
+        childrenColProps: { span: 24 },
+        children: [
+          { label: '作品/产品名称', name: 'title', type: 'text', additionalProps: { placeholder: '请输入标题' } },
+          { 
+            label: '所属区域', name: 'region', type: 'select-id', 
+            additionalProps: { 
+              placeholder: '请选择地区', 
+              loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ text: p.title, value: p.id, raw: p })) 
+            } 
+          },
+          { label: '类型', name: 'type', type: 'select', 
+            additionalProps: { 
+              placeholder: '请选择类型', 
+              columns: [[
+                { text: '文字', value: 1 }, 
+                { text: '音频', value: 2 }, 
+                { text: '视频', value: 3 }, 
+                { text: '相册', value: 4 }, 
+                { text: '其他类型', value: 5 }
+              ]], 
+            } 
+          },
+          { 
+            label: '缩略图', name: 'image', type: 'uploader', 
+            additionalProps: { 
+              placeholder: '请上传图片', 
+              //uploadCo: useAliOssUploadCo('inheritor/images'), 
+              name: 'file', 
+              accept: 'image/*' 
+            }
+          },
+          { label: '图片说明', name: 'imageDesc', type: 'text', additionalProps: { placeholder: '请输入图片说明' } },
+          { 
+            label: '组图', name: 'images', type: 'uploader', 
+            show: { callback: (_, model) => (model as InheritorWorkInfo).type == 4 },
+            additionalProps: { 
+              placeholder: '请上传组图', 
+              accept: 'image/*',
+              // beforeUpload: useBeforeUploadImageChecker(),
+              // uploadCo: useAliOssUploadCo('inheritor/images'), name: 'file', maxCount: 20 
+            } 
+          },
+          { label: '作品/产品介绍', name: 'content', type: 'richtext', additionalProps: { placeholder: '请输入内容介绍' } },
+          { 
+            label: '音频', name: 'audio', type: 'uploader', 
+            show: { callback: (_, model) => (model as InheritorWorkInfo).type == 2 },
+            additionalProps: { 
+              placeholder: '请上传音频', 
+              accept: 'audio/*',
+              // beforeUpload: useBeforeUploadAudioChecker(),
+              // uploadCo: useAliOssUploadCo('inheritor/audios'), 
+              name: 'file' 
+            } 
+          },
+          { 
+            label: '相关视频', name: 'video', type: 'uploader', 
+            show: { callback: (_, model) => (model as InheritorWorkInfo).type === 3 },
+            additionalProps: { 
+              // beforeUpload: useBeforeUploadVideoChecker(),
+              // placeholder: '请上传视频', uploadCo: useAliOssUploadCo('inheritor/videos'), name: 'file' 
+            } 
+          },
+          { 
+            label: '数字档案', name: 'archives', type: 'uploader', 
+            show: { callback: (_, model) => (model as InheritorWorkInfo).type === 5 },
+            additionalProps: { 
+              placeholder: '请上传数字档案', 
+              //uploadCo: useAliOssUploadCo('inheritor/archives'), 
+              name: 'file', 
+              maxCount: 100 
+            } 
+          },
+          { 
+            label: '审核人员', name: 'text1', type: 'static-text', 
+            additionalProps: {
+              text: '黄念旭,李向群,卢志明',
+              style: { color: '#999', }
+            }
+          },
+          { 
+            label: '审核状态', name: 'text2', type: 'static-text', 
+            additionalProps: {
+              text: '暂未审核',
+              style: { color: '#999', }
+            }
+          },
+          { 
+            label: '填报人', name: 'text3', type: 'static-text', 
+            additionalProps: {
+              text: authStore.userInfo?.nickname ,
+            }
+          },
+        ]
+      },
+    ],
+  formRules: {
+    title: [{ required: true, message: '请输入标题' }],
+    region: [{ required: true, message: '请选择地区' }],
+    image: [{ required: true, message: '请上传图片' }],
+    type: [{ required: true, message: '请选择类型' }],
+    content: [{ required: true, message: '请输入内容介绍' }],
+  }
+});
+
+const { querys } = useLoadQuerys({
+  id: 0,
+  inheritorId: 0,
+})
+
+async function loadData() {
+  const id = querys.value.id;
+  if (id)
+    formModel.value = await InheritorContent.getIchWorksDetail(id);
+}
+async function handleSave(data: InheritorWorkInfo) {
+  data.ichId = querys.value.id;
+  data.inheritorId = querys.value.inheritorId;
+  return data;
+}
+
+</script>

+ 60 - 1
src/pages/collect/inheritor.vue

@@ -1,6 +1,65 @@
 <template>
-  <text>TODO:inheritor</text>
+  <FlexCol :innerStyle="{
+    backgroundImage: 'url(https://xy.wenlvti.net/app_static/images/mine/TopBanner.png)',
+    backgroundSize: '100% auto',
+    backgroundRepeat: 'no-repeat',
+    backgroundPosition: 'top center',
+    minHeight: '100vh',
+  }">
+    <StatusBarSpace />
+    <NavBar leftButton="back" />
+    <FlexCol gap="gap.xl" padding="space.lg">
+      <FlexCol center>
+        <Text fontConfig="h2">非遗数字化资源信息校对</Text>
+        <FlexRow>
+          <Text fontConfig="subText">技术支持:18649931391</Text>
+        </FlexRow>
+      </FlexCol>
+      <FlexRow justify="space-between">
+        <FlexRow align="center" gap="gap.md">
+          <Avatar 
+            randomColor
+            :src="authStore.userInfo?.avatar" 
+            defaultAvatar="https://mncdn.wenlvti.net/app_static/minnan/logo.png"
+          />
+          <Text fontConfig="h4">{{ authStore.userInfo?.nickname }}</Text>
+        </FlexRow>
+        <FlexRow>
+          <WxButton openType="contact">
+            <Button type="text" icon="wechat">在线客服</Button>
+          </WxButton>
+          <Button type="text" icon="lock" @click="navTo('user/change-password')">修改密码</Button>
+        </FlexRow>
+      </FlexRow>
+      <CellGroup round>
+        <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/9fb29e8bdb66490034145c90f892773a.png" title="自查评估" showArrow touchable @click="navTo('assessment/index')" />
+      </CellGroup>
+      <CellGroup round>
+        <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/1366973c061bf98594036e42c0344593.png" title="非遗项目" showArrow touchable @click="navTo('forms/ich')" />
+        <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/042236758da5aaed21c1010e5b9440ce.png" title="传承人" showArrow touchable @click="navTo('forms/inheritor')" />
+        <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/d2e9010323d098aa51e268fc32f14d3d.png" title="传习所" showArrow touchable @click="navTo('forms/seminar')" />
+        <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/66d4665b1da5075e60148312469b2630.png" title="作品" showArrow touchable @click="navTo('works')" />
+      </CellGroup>
+    </FlexCol>
+    <XBarSpace />
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
+import { useAuthStore } from '@/store/auth';
+import CellGroup from '@/components/basic/CellGroup.vue';
+import Cell from '@/components/basic/Cell.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import WxButton from '@/components/basic/WxButton.vue';
+import XBarSpace from '@/components/layout/space/XBarSpace.vue';
+import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
+import NavBar from '@/components/nav/NavBar.vue';
+import Avatar from '@/components/display/Avatar.vue';
+import Button from '@/components/basic/Button.vue';
+import { navTo } from '@/components/utils/PageAction';
+
+const authStore = useAuthStore();
+
 </script>

+ 145 - 0
src/pages/collect/user/change-password.vue

@@ -0,0 +1,145 @@
+<template>
+  <CommonRoot>
+    <FlexCol gap="gap.xl" padding="space.lg">
+      <FlexCol radius="radius.md" backgroundColor="white" overflow="hidden">
+        <template v-if="isSuccess">
+          <Result
+            status="success"
+            title="修改成功"
+            description="您可以使用新密码登录"
+          />
+        </template>
+        <template v-else>
+          <DynamicForm
+            ref="formRef"
+            :model="formModel"
+            :options="formDefine"
+          />
+        </template>
+      </FlexCol>
+      <FlexCol gap="gap.md">
+        <Button type="primary" block :loading="loading" @click="handleSubmit">
+          修改密码
+        </Button>
+        <Button block @click="back()">
+          返回
+        </Button>
+      </FlexCol>
+      <XBarSpace />
+    </FlexCol>
+  </CommonRoot>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { back } from '@/components/utils/PageAction';
+import UserApi from '@/api/auth/UserApi';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import type { FormProps } from '@/components/form/Form.vue';
+import type { RuleItem } from 'async-validator';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import Result from '@/components/feedback/Result.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
+import Button from '@/components/basic/Button.vue';
+import XBarSpace from '@/components/layout/space/XBarSpace.vue';
+import { alert, toast } from '@/components/dialog/CommonRoot';
+
+const formRef = ref<IDynamicFormRef>();
+const loading = ref(false);
+const isSuccess = ref(false);
+
+const formModel = ref({
+  oldPassword: '',
+  password: '',
+  passwordRepeat: '',
+});
+
+const formDefine: IDynamicFormOptions = {
+  formAdditionaProps: {
+    labelFlex: 4,
+    inputFlex: 8,
+  } as FormProps,
+  formRules: {
+    oldPassword: [
+      { required: true, message: '请输入旧密码' },
+      { min: 6, message: '密码长度必须大于等于6位' },
+    ],
+    password: [
+      { required: true, message: '请输入密码' },
+      { min: 6, message: '密码长度必须大于等于6位' },
+    ],
+    passwordRepeat: [
+      { required: true, message: '请再输入一次密码' },
+      {
+        //表单自定义校验
+        validator(rule, value, callback, source) {
+          if (value !== formModel.value.password) {
+            callback('两次输入密码不一致,请检查');
+            return;
+          }
+          callback();
+        },
+      },
+    ] as RuleItem[],
+  },
+  formItems: [
+    {
+      label: '旧密码',
+      name: 'oldPassword',
+      type: 'password',
+      additionalProps: {
+        placeholder: '请输入旧密码' 
+      }
+    },
+    {
+      label: '新密码',
+      name: 'password',
+      type: 'password',
+      additionalProps: {
+        placeholder: '请输入密码' 
+      }
+    },
+    {
+      label: '确认新密码',
+      name: 'passwordRepeat',
+      type: 'password',
+      additionalProps: {
+        placeholder: '请输入新密码' 
+      }
+    },
+  ],
+};
+
+async function handleSubmit() {
+  if (!formRef.value)
+    return;
+  try {
+    await formRef.value.validate();
+  } catch {
+    uni.showToast({
+      title: '有必填项未填写,请检查',
+      icon: 'none',
+    });
+    return;
+  }
+  loading.value = true;
+  try {
+    await UserApi.updatePassword({
+      oldpassword: formModel.value.oldPassword,
+      newpassword: formModel.value.password,
+    });
+    toast('修改密码成功');
+    isSuccess.value = true;
+  } catch (error) {
+    alert({
+      title: '修改密码失败',
+      content: '' + error,
+      icon: 'error',
+    });
+  } finally {
+    loading.value = false;
+  }
+}
+
+</script>

+ 26 - 0
src/pages/collect/works.vue

@@ -0,0 +1,26 @@
+<template>
+  <SimplePageContentLoader :loader="ichData">
+    <SimpleList
+      v-if="ichData.status.value !== 'error' && ichData.status.value !== 'loading'"
+      :data="ichData.content.value || []"
+      @itemClick="(item) => navTo('collect/forms/works', { id: item.id })"
+    />
+  </SimplePageContentLoader>
+</template>
+
+<script setup lang="ts">
+import InheritorContent from '@/api/collect/InheritorContent';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import SimpleList from '@/components/list/SimpleList.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import { navTo } from '@/components/utils/PageAction';
+
+const ichData = useSimpleDataLoader(async () => {
+  const data = await InheritorContent.getIchInfo(undefined);
+  return await InheritorContent.getIchWorksInfo({
+    ichId: data.id,
+    page: 1,
+    pageSize: 100,
+  });
+});
+</script>

+ 2 - 2
src/pages/user/index.vue

@@ -50,7 +50,7 @@
             <FlexCol>
               <Text fontConfig="h4" v-if="userInfo" color="white" :text="userInfo.nickname || `用户${userInfo.id}`" />
               <Text fontConfig="h4" v-else color="white" text="欢迎登录" />
-              <Text color="white" v-if="userInfo" :text="`守护编号 ${userInfo.id} 积分 ${userInfo.totalCheckins}`" />
+              <Text color="white" v-if="userInfo" :text="`守护编号 ${userInfo.id} 积分 ${userInfo.totalCheckins || 0}`" />
             </FlexCol>
           </FlexRow>
           <Icon icon="arrow-right-bold" color="white" />
@@ -67,7 +67,7 @@
           </Cell>
           <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/66d4665b1da5075e60148312469b2630.png" title="我的投稿" showArrow touchable @click="goContributeList" />
           <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/042236758da5aaed21c1010e5b9440ce.png" title="我的收藏" showArrow touchable @click="goCollectList" />
-          <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/acd97ca7b3f7736942495c7aec1dd65b.png" title="传承人" showArrow touchable @click="goInheritor" />
+          <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/1366973c061bf98594036e42c0344593.png" title="传承人之家" showArrow touchable @click="goInheritor" />
           <button open-type="contact" class="remove-button-style">
             <Cell icon="https://mncdn.wenlvti.net/uploads/20250313/d2e9010323d098aa51e268fc32f14d3d.png" title="在线客服" showArrow touchable />
           </button>