Sfoglia il codice sorgente

📦 表单入口按要求动态与修改

快乐的梦鱼 2 mesi fa
parent
commit
40f837ecfa

+ 40 - 0
src/api/inhert/VillageApi.ts

@@ -111,6 +111,38 @@ export class VolunteerInfo extends DataModel<VolunteerInfo> {
   statusText = '';
 }
 
+export class VillageCatalogListItem extends DataModel<VillageCatalogListItem> {
+  constructor() {
+    super(VillageCatalogListItem, "村落目录列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      haschild: { clientSide: 'boolean', serverSide: 'number' },
+    }
+    this._nameMapperServer = {
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+
+  }
+  id !: number;
+  name = '';
+  title = '';
+  icon = '';
+  desc = '';
+  pid = 0;
+  collectModuleId = 0;
+  collectModuleName = '';
+  villageName = '';
+  spacer = '';
+  haschild = false;
+}
 export class VillageMenuListItem extends DataModel<VillageMenuListItem> {
   constructor() {
     super(VillageMenuListItem, "村落菜单列表");
@@ -224,6 +256,14 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
       .catch(e => { throw e });
   }
   
+  async getCatalogList(villageId?: number, pid?: number) {
+    return (this.get('/village/village/getCatalogList', '村落目录列表', {
+      village_id: villageId,
+      pid: pid === 0 ? undefined : pid,
+    })) 
+      .then(res => transformArrayDataModel<VillageCatalogListItem>(VillageCatalogListItem, res.data2, `村落目录列表`, true))
+      .catch(e => { throw e });
+  }
   async getVillageMenuList(id: number) {
     return (this.get('/village/menu/getList', '村落菜单列表', {
       village_id: id,

+ 2 - 2
src/assets/scss/colors.scss

@@ -1,5 +1,5 @@
-$primary-color: #f09115;
-$primary-dark-color: #db8719;
+$primary-color: #00ca76;
+$primary-dark-color: #00814b;
 
 $text-color: #333;
 $text-color-light: #fff;

+ 6 - 5
src/pages/components/TaskItem.vue

@@ -1,17 +1,18 @@
 <template>
   <div class="item">
-    <i :class="'iconfont ' + icon"></i>
+    <img v-if="icon && icon.startsWith('http')" :src="icon" alt="" />
+    <i v-else :class="'iconfont ' + icon"></i>
     <div class="info">
       <div class="title">{{title}}</div>
       <div class="desc">{{desc}}</div>
     </div>
     <a-button 
       type="primary" 
-      :disabled="!enabled" 
-      :title="enabled ? '去编写内容吧' : '当前栏目未开放,请联系管理员认领可采编栏目'"
+      :disabled="!enable" 
+      :title="enable ? '去编写内容吧' : '当前栏目未开放,请联系管理员认领可采编栏目'"
       @click="emit('click')"
     >
-      {{enabled ? '采编' : '待开放'}}
+      {{enable ? '采编' : '待开放'}}
     </a-button>
   </div>
 </template>
@@ -31,7 +32,7 @@ const props = defineProps({
     type: String,
     default: ''
   },
-  enabled: {
+  enable: {
     type: Boolean,
     default: false
   }

+ 1 - 1
src/pages/components/TaskList.vue

@@ -36,7 +36,7 @@
       }
     }
 
-    .iconfont {
+    img, .iconfont {
       width: 50px;
       height: 50px;
       border-radius: 50%;

+ 2 - 1
src/pages/composeable/TaskEntryForm.ts

@@ -7,7 +7,7 @@ export function useTaskEntryForm() {
     villageVolunteerId: 0,
   });
   
-  function goForm(subType: string, subId: number, subKey = 'type', type = 'list') {
+  function goForm(subType: string, subId: number, subKey = 'type', type = 'list', subTitle = '') {
     router.push({
       path: '../forms/' + type, 
       query: {
@@ -17,6 +17,7 @@ export function useTaskEntryForm() {
         subType,
         subId,
         subKey,
+        subTitle,
       }
     })
   }

+ 65 - 73
src/pages/details.vue

@@ -40,72 +40,17 @@
             title="随手记" 
             desc="写随手记,记录下村庄文化发现和思考,自动分类。也可点击下方进入指定的分类采集信息" 
             icon="icon-task-summary" 
-            :enabled="canCollect('collect')"
+            :enable="canCollect('collect')"
             @click="goForm('collect', 1)"
           />
-
-          <TaskItem 
-            title="村落概况" 
-            desc="探索村落的历史渊源与发生轨迹" 
-            icon="icon-task-summary" 
-            :enabled="canCollect('village')"
-            @click="navTo('task/summary', nextPageData)"
-          />
-          <TaskItem 
-            title="历史文化" 
-            desc="传承百年文化遗产和精神财富" 
-            icon="icon-task-history" 
-            :enabled="canCollect('cultural')"
-            @click="navTo('task/history', nextPageData)"
-          />
-          <TaskItem 
-            title="非物质文化遗产项目" 
-            desc="维护文化多样性" 
-            icon="icon-task-custom-1" 
-            :enabled="canCollect('ich')"
-            @click="goForm('ich', 0)"
-          />
-          <TaskItem 
-            title="环境格局" 
-            desc="感受自然人文环境之美" 
-            icon="icon-task-environment" 
-            :enabled="canCollect('environment')"
-            @click="navTo('task/environment', nextPageData)"
-          />
-          <TaskItem 
-            title="传统建筑" 
-            desc="领略古建筑的独特魅力" 
-            icon="icon-task-building" 
-            :enabled="canCollect('building')"
-            @click="navTo('task/building', nextPageData)"
-          />
-          <TaskItem 
-            title="民俗文化" 
-            desc="体验民间传统习俗与节庆" 
-            icon="icon-task-custom" 
-            :enabled="canCollect('folk_culture')"
-            @click="navTo('task/custom', nextPageData)"
-          />
-          <TaskItem 
-            title="美食物产" 
-            desc="正宗、传统地方特色美食" 
-            icon="icon-task-food" 
-            :enabled="canCollect('food_product')"
-            @click="navTo('task/food', nextPageData)"
-          />
-          <TaskItem 
-            title="物产资源" 
-            desc="特定地域的植物、矿物或手工艺" 
-            icon="icon-task-mine" 
-            :enabled="canCollect('food_product')"
-            @click="navTo('task/mine', nextPageData)"
-          />
-          <TaskItem 
-            title="旅游路线" 
-            desc="体验独特的文化魅力" 
-            icon="icon-task-trip" 
-            :enabled="canCollect('route')"
-            @click="navTo('task/trip', nextPageData)"
+          <TaskItem
+            v-for="(item,key) in taskList.content.value"
+            :key="key"
+            :icon="item.icon"
+            :title="item.title"
+            :desc="item.desc"
+            :enable="typeof item.enable === 'string' ? canCollect(item.enable) : item.enable"
+            @click="goTask(item)"
           />
         </TaskList>
 
@@ -115,39 +60,86 @@
 </template>
 
 <script setup lang="ts">
-import { useLoadQuerys } from '@imengyu/imengyu-web-shared';
+import { useLoadQuerys, useSimpleDataLoader } from '@imengyu/imengyu-web-shared';
 import { computed, h } from 'vue';
 import { useRouter } from 'vue-router';
 import { ArrowLeftOutlined } from '@ant-design/icons-vue';
 import { useCollectStore } from '@/stores/collect';
 import { useTaskEntryForm } from './composeable/TaskEntryForm';
 import { useAuthStore } from '@/stores/auth';
+import { Alert, Modal } from 'ant-design-vue';
+import { TaskRootDef, type TaskRootMenuDefItem } from './forms/tasks';
+import VillageApi, { VillageCatalogListItem } from '@/api/inhert/VillageApi';
 import TaskList from './components/TaskList.vue';
 import TaskItem from './components/TaskItem.vue';
-import { Alert } from 'ant-design-vue';
 
 const authStore = useAuthStore();
 const router = useRouter();
 const { querys } = useLoadQuerys({ 
-  id: 0,  
   name: '',
   points: 0,
   level: 0,
+  villageId: 0,  
   villageVolunteerId: 0,
 });
 const nextPageData = computed(() => ({
-  villageId: querys.value.id,  
+  villageId: querys.value.villageId,  
   villageVolunteerId: querys.value.villageVolunteerId,
 }));
 const { isAdmin } = useAuthStore();
 const { canCollect, isEmpty } = useCollectStore();
 const { goForm } = useTaskEntryForm();
 
-function navTo(path: string, data: any) {
-  router.push({
-    path,
-    query: data,
-  })
+const taskList = useSimpleDataLoader<(TaskRootMenuDefItem & {
+  catalogItem?: VillageCatalogListItem,
+})[]>(async () => {
+  const res = (await VillageApi.getCatalogList(querys.value.villageId))
+    .filter(item => item.pid == 0);
+  if (res.length === 0)
+    return TaskRootDef;
+  return res
+    .map(item => ({
+      ...item,
+      enable: true,
+      catalogItem: item,
+      goForm: { 
+        title: item.title, 
+        name: 'force' 
+      },
+    } as TaskRootMenuDefItem));
+});
+
+function goCollect() {
+  if (!canCollect('collect')) {
+    Modal.error({
+      content: '您当前没有可采编随手记的权限',
+    });
+    return;
+  }
+  goForm('collect', 1, '随手记')
+}
+
+function goTask(item: TaskRootMenuDefItem & {
+  catalogItem?: VillageCatalogListItem,
+}) {
+  if (!item.enable) {
+    Modal.error({
+      content: '您当前没有可完成此任务的权限',
+    });
+    return;
+  }
+  if (item.goForm instanceof Array) {
+    goForm(...item.goForm)
+  } else {
+    router.push({
+      path: '/forms/task',
+      query: {
+        ...nextPageData.value,
+        taskPid: item.catalogItem?.id || 0,
+        taskName: item.goForm.name,
+      }
+    })
+  }
 }
 </script>
 

+ 123 - 60
src/pages/forms/common.vue

@@ -6,6 +6,12 @@
       <div class="content">
         <a-button :icon="h(ArrowLeftOutlined)" class="mb-3" @click="handleBack">返回上一页</a-button>
 
+        <Alert
+          v-if="localSavedState"
+          type="info"
+          message="您的修改已经暂存,可以放心离开,下次接着编辑"
+          class="mb-3"
+        />
         <div class="form-box">
           <a-spin :spinning="loading">
             <DynamicForm
@@ -20,7 +26,7 @@
             <div class="d-flex flex-column mt-3">
               <span>
                 <ExclamationCircleOutlined class="me-2" />
-                提示:上传文件时请勿离开页面防止上传失败,离开之前请保存您的修改以防丢失
+                提示:上传文件时请勿离开页面防止上传失败。
               </span>
               <div class="d-flex flex-row w-100 align-items-center justify-content-end mt-3">  
                 <a-button 
@@ -41,28 +47,84 @@
 </template>
 
 <script setup lang="ts">
-import { ref, h, reactive, nextTick, type Ref } from 'vue';
-import { getVillageInfoForm } from './forms';
-import { RequestApiError, waitTimeOut } from '@imengyu/imengyu-utils';
+import { ref, h, reactive, nextTick, type Ref, watch } from 'vue';
+import { getVillageInfoForm, type SingleForm } from './forms';
+import { Debounce, RequestApiError, SettingsUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
 import { logError, useLoadQuerys, usePageAction } from '@imengyu/imengyu-web-shared';
-import { message, Modal } from 'ant-design-vue';
+import { Alert, message, Modal } from 'ant-design-vue';
 import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
 import type { FormInstance } from 'ant-design-vue/es/form/Form';
 import { ArrowLeftOutlined } from '@ant-design/icons-vue';
-import VillageInfoApi from '@/api/inhert/VillageInfoApi';
+import VillageInfoApi, { CommonInfoModel } from '@/api/inhert/VillageInfoApi';
 import { useCollectStore } from '@/stores/collect';
 
 const loading = ref(false);
 const formRef = ref<IDynamicFormRef>();
 const formModel = ref<any>();
 const formOptions = ref<IDynamicFormOptions>();
+let currentFormInfo : SingleForm|null = null;
+
 const {
   backAndCallOnPageBack,
   back,
 } = usePageAction();
 const { getCollectModuleId } = useCollectStore();
 
+const { querys } = useLoadQuerys({ 
+  villageId: 0,  
+  villageVolunteerId: 0,
+  subType: '',
+  subKey: '',
+  subId: 0,
+  id: 0,
+}, async (querys) => {
+  loading.value = true;
+  if (!formRef.value)
+    return;
+
+  await waitTimeOut(400);
+
+  let formData = undefined;
+  try {
+
+    currentFormInfo = getVillageInfoForm(querys.subType, querys.subId);
+    const [model, forms] = currentFormInfo;
+    formModel.value = new model();
+    formOptions.value = forms(formRef as Ref<IDynamicFormRef>);
+    if (querys.id !== -1) {
+      formData = await VillageInfoApi.getInfo(
+        getCollectModuleId(querys.subType),
+        querys.subType, 
+        querys.subId,
+        querys.subKey,
+        querys.villageId, 
+        querys.villageVolunteerId,
+        querys.id,
+        model,
+      );
+      console.log('Load form ', formData);
+    }
+    await waitTimeOut(500);
+  } catch (e) {
+    if (!(e instanceof RequestApiError && e.errorMessage.startsWith('请完成')))
+      showError(e, undefined, () => backPrev(false));
+    console.error(e);
+  } finally {
+    loading.value = false;
+  }
+  formData = await loadLocalSave(formData as CommonInfoModel);
+  if (formData)
+    formModel.value = reactive(formData);
+
+  await nextTick();
+  formRef.value.initDefaultValuesToModel();
+
+  await waitTimeOut(1000);
+  canSaveNow = true;
+});
+
+
 async function submit() {
   if (!formRef.value)
     return;
@@ -109,65 +171,66 @@ function backPrev(needRefresh: boolean) {
   });
 }
 function handleBack() {
-  Modal.confirm({
-    title: '如果有修改请先提交,未保存的修改将丢失,您确认返回上一页吗?',
-    okText: '确认',
-    okType: 'danger',
-    onOk() {
-      back();
-    },
-  });
+  if (!localSavedState.value)
+    Modal.confirm({
+      title: '如果有修改请先提交,未保存的修改将丢失,您确认返回上一页吗?',
+      okText: '确认',
+      okType: 'danger',
+      onOk() {
+        back();
+      },
+    });
+  else
+    back();
 }
 
-const { querys } = useLoadQuerys({ 
-  villageId: 0,  
-  villageVolunteerId: 0,
-  subType: '',
-  subKey: '',
-  subId: 0,
-  id: 0,
-}, async (querys) => {
-  loading.value = true;
-  if (!formRef.value)
-    return;
-
-  await waitTimeOut(400);
+const localSavedState = ref(false);
+const saveLocalSaveDebounce = new Debounce(1000, saveLocalSave);
+let canSaveNow = false;
 
-  let formData = undefined;
-  try {
-
-    const [model, forms] = getVillageInfoForm(querys.subType, querys.subId);
-    formModel.value = new model();
-    formOptions.value = forms(formRef as Ref<IDynamicFormRef>);
-    if (querys.id !== -1) {
-      formData = await VillageInfoApi.getInfo(
-        getCollectModuleId(querys.subType),
-        querys.subType, 
-        querys.subId,
-        querys.subKey,
-        querys.villageId, 
-        querys.villageVolunteerId,
-        querys.id,
-        model,
-      );
-      console.log('Load form ', formData);
-    }
-    await waitTimeOut(500);
-  } catch (e) {
-    if (!(e instanceof RequestApiError && e.errorMessage.startsWith('请完成')))
-      showError(e, undefined, () => backPrev(false));
-    console.error(e);
-  } finally {
-    loading.value = false;
+function getSaveName() {
+  return `FormLocalSave-${querys.value.id}-${querys.value.subType}-${querys.value.subId}-${querys.value.villageId}-${querys.value.villageVolunteerId}`;
+}
+async function loadLocalSave(formData: CommonInfoModel|undefined) {
+  if (!currentFormInfo)
+    return formData;
+  console.log('加载暂存数据');
+  const saveName = getSaveName();
+  const saveData = SettingsUtils.getSettings(saveName, '');
+  if (saveData) {
+    const res = await Modal.confirm({
+      content: '您有上次编辑未完成的内容,是否要从上次的编辑数据继续?',
+      cancelText: '取消',
+      okText: '继续',
+      icon: h(ExclamationCircleOutlined),
+    })
+    if (res) {
+      const d = JSON.parse(saveData);
+      d.id = 0;
+      formData = new currentFormInfo[0]().fromServerSide(d) as CommonInfoModel;  
+      console.log('有暂存数据', formData);
+    } else
+      deleteLocalSave();
   }
+  return formData;
+}
+function deleteLocalSave() {
+  const saveName = getSaveName();
+  SettingsUtils.getSettings(saveName, '');
+  localSavedState.value = false;
+}
+function saveLocalSave() {
+  const saveName = getSaveName();
+  SettingsUtils.setSettings(saveName, JSON.stringify(formModel.value.toServerSide()));
+  localSavedState.value = true;
+  console.log('保存暂存数据');
+}
 
-  if (formData) {
-    formModel.value = reactive(formData);
-  }
-    
-  await nextTick();
-  formRef.value.initDefaultValuesToModel();
-});
+watch(formModel, () => {
+  if (!canSaveNow)
+    return;
+  saveLocalSaveDebounce.executeWithDelay();
+}, { deep: true })
 
 function showError(e: any, title?: string, callback?: () => void) {
   Modal.error({

+ 2 - 2
src/pages/forms/data/building.ts

@@ -507,7 +507,7 @@ export function villageInfoBuildingForm(title: string) : SingleForm  {
         ]
       },
     ] 
-  })]
+  }), { title: title, typeName: 'nature' }]
 }
 
 export const villageInfoDistributionForm : SingleForm = [CommonInfoModel, (r) => ({
@@ -562,4 +562,4 @@ export const villageInfoDistributionForm : SingleForm = [CommonInfoModel, (r) =>
       showTitle: false,
     }).formItems
   ] 
-})];
+}), { title: '建筑分布', typeName: '', }];

+ 5 - 5
src/pages/forms/data/cultural.ts

@@ -56,18 +56,18 @@ export function villageInfoFolkCultureForm(title: string) : SingleForm {
         contentKey: 'details'
       }).formItems
     ]
-  })];
+  }), { title: title, typeName: 'folk_culture_type', }];
 }
 
 export const villageInfoCulture : GroupForm = {
   [1]: [CommonInfoModel, (m) => villageCommonContent(m, {
     title: '建村历史',
     showTitle: true
-  })],
+  }), { title: '建村历史', typeName: 'cultural_type', }],
   [2]: [CommonInfoModel, (m) => villageCommonContent(m, {
     title: '历史事件',
     showTitle: true
-  })],  
+  }), { title: '历史事件', typeName: 'cultural_type', }],  
   [3]: [CommonInfoModel, (m) => ({
     formItems: [
       ...(villageCommonContent(m, {
@@ -90,7 +90,7 @@ export const villageInfoCulture : GroupForm = {
         }]
       },
     ],
-  })],
+  }), { title: '历史文献', typeName: 'cultural_type', }],
   [4]: [CommonInfoModel, (m) => ({
     formItems: [
       ...villageCommonContent(m, {
@@ -189,5 +189,5 @@ export const villageInfoCulture : GroupForm = {
         },
       }, */
     ],
-  })],
+  }), { title: '口述历史', typeName: 'cultural_type', }],
 }

+ 1 - 1
src/pages/forms/data/element.ts

@@ -256,4 +256,4 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
       ]
     },
   ] 
-})]
+}), { title: '环境要素', typeName: '', }]

+ 1 - 1
src/pages/forms/data/environment.ts

@@ -98,4 +98,4 @@ export const villageInfoEnvironmentForm : SingleForm= [CommonInfoModel, (r) => (
       showTitle: false,
     }).formItems
   ] 
-})]
+}), { title: '环境格局', typeName: '', }]

+ 1 - 1
src/pages/forms/data/food.ts

@@ -39,5 +39,5 @@ export function villageInfoFoodProductsForm(title: string) : SingleForm {
         showTitle: false,
       })).formItems
     ]
-  })];
+  }), { title: title, typeName: 'product_type', }];
 }

+ 2 - 2
src/pages/forms/data/history.ts

@@ -36,7 +36,7 @@ export const villageInfoStoryFormItems: SingleForm = [CommonInfoModel, (r) => ({
       showTitle: false,
     }).formItems
   ]
-})]
+}), { title: '掌故轶事', typeName: '', }]
 
 export const villageInfoFigureFormItems: SingleForm = [CommonInfoModel, (r) => ({
   formItems: [
@@ -73,4 +73,4 @@ export const villageInfoFigureFormItems: SingleForm = [CommonInfoModel, (r) => (
       showTitle: false,
     }).formItems
   ]
-})]
+}), { title: '历史人物', typeName: '', }]

+ 1 - 1
src/pages/forms/data/ich.ts

@@ -241,4 +241,4 @@ export const ichFormItems : SingleForm = [CommonInfoModel, (m) => ({
       ]
     },
   ] 
-})]
+}), { title: '非遗基础信息', typeName: '', }]

+ 10 - 5
src/pages/forms/data/overview.ts

@@ -86,7 +86,7 @@ export const villageInfoOverviewForm : GroupForm = {
         }],
       },
     ]
-  })],
+  }), { title: '行政区划', typeName: '', }],
   [2]: [VillageEnvInfo, () => ({
     formItems: [
       { 
@@ -192,7 +192,7 @@ export const villageInfoOverviewForm : GroupForm = {
         }],
       },
     ]
-  })],
+  }), { title: '地理信息', typeName: '', }],
   [3]: [CommonInfoModel, () => ({
     formItems: [
       { 
@@ -290,7 +290,7 @@ export const villageInfoOverviewForm : GroupForm = {
         rules:  [] 
       }, 
     ]
-  })],
+  }), { title: '建设与保护', typeName: '', }],
   [4]: [CommonInfoModel, () => ({ 
     formItems: [
       {
@@ -336,6 +336,11 @@ export const villageInfoOverviewForm : GroupForm = {
             rowProps: { 
               align: 'center',
             }  as RowProps,
+            additionalProps: {
+              style: {
+                padding: '0',
+              }
+            },
             children: [
               { 
                 label: '', 
@@ -481,7 +486,7 @@ export const villageInfoOverviewForm : GroupForm = {
         ]
       },
     ] 
-  })],
+  }), { title: '人口与经济', typeName: '', }],
   [5]: [CommonInfoModel, () => ({
     formItems: [
       {
@@ -541,5 +546,5 @@ export const villageInfoOverviewForm : GroupForm = {
         ]
       },
     ] 
-  })],
+  }), { title: '村落综述', typeName: '', }],
 }

+ 1 - 1
src/pages/forms/data/relic.ts

@@ -263,4 +263,4 @@ export const villageInfoRelicForm : SingleForm = [CommonInfoModel, (r) => ({
 
     
   ] 
-})];
+}), { title: '文物古迹', typeName: '', }];

+ 1 - 1
src/pages/forms/data/specker.ts

@@ -119,4 +119,4 @@ export const villageInfoSpeakerForm : SingleForm = [CommonInfoModel, (r) => ({
       ]
     },
   ]
-})]
+}), { title: '口述人', typeName: '', }]

+ 258 - 237
src/pages/forms/data/travel.ts

@@ -4,7 +4,7 @@ import type { PickerIdFieldProps } from "@/components/dynamic/wrappers/PickerIdF
 import type { FieldProps } from "@/components/form/Field.vue";
 import type { PickerFieldProps } from "@/components/form/PickerField.vue";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
-import type { GroupForm, SingleForm } from "../forms";
+import type { SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
 
 export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, () => ({
@@ -460,243 +460,264 @@ export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, () => (
       ]
     }
   ] 
-})]
+}), { title: '旅游导览', typeName: '', }]
 
-export const villageInfoRouteForm : GroupForm = {
-  [1]: [CommonInfoModel, (r) => ({
-    formItems: [
-      {
-        label: '游览路线', 
-        name: 'route', 
-        type: 'text', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入游览路线',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入游览路线',
-        }] 
-      },
-      {
-        label: '路线名称', 
-        name: 'name', 
-        type: 'text', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入路线名称',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入路线名称',
-        }] 
-      },
-      {
-        label: '描述', 
-        name: 'desc', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入描述',
-          maxLength: 200,
-          showWordLimit: true, 
-        },
-        rules:  [{
-          required: true,
-          message: '请输入描述',
-        }] 
-      },
-      {
-        label: '起始点', 
-        name: 'startPoint', 
-        type: 'text', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入起始点',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入起始点',
-        }] 
-      },
-      {
-        label: '终止点', 
-        name: 'endPoint', 
-        type: 'text', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入终止点',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入终止点',
-        }] 
-      },
-      {
-        label: '预计时长', 
-        name: 'estimate', 
-        type: 'number', 
-        defaultValue: 1,
-        additionalProps: {
-          min: 1,
-          addonAfter: '小时',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入预计时长',
-        }] 
-      },
-      ...villageCommonContent(r, {
-        title: '游览路线',
-        showContent: false,
-        showTitle: false,
-      }).formItems
-    ] 
-  })],
-  [2]: [CommonInfoModel, (r) => ({
-    formItems: [
-      {
-        label: '活动标题', 
-        name: 'activity', 
-        type: 'text', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入活动标题',
-        },
-        rules:  [{
-          required: true,
-          message: '请输入活动标题',
-        }] 
-      },
-      {
-        label: '活动开始时间', 
-        name: 'startTime',
-        type: 'datetime', 
-        defaultValue: '',
-        additionalProps: {
-          type: 'datetime',
-        },
-        rules: [{
-          required: true,
-          message: '请选择活动开始时间',
-        }],
-      },
-      {
-        label: '活动结束时间', 
-        name: 'endTime',
-        type: 'datetime', 
-        defaultValue: '',
-        additionalProps: {
-          type: 'datetime',
-        },
-        rules: [{
-          required: true,
-          message: '请选择活动结束时间',
-        }],
-      },
-      {
-        label: '活动时长', 
-        name: 'duration',
-        type: 'number', 
-        defaultValue: 0,
-        additionalProps: {
-          min: 0,
-          addonAfter: '分钟',
-        },
-        rules: [{
-          required: true,
-          message: '请选择活动时长',
-        }],
-      },
-      ...villageCommonContent(r, {
-        title: '活动时间',
-        showContent: false,
-        showTitle: false,
-      }).formItems
-    ] 
-  })],
-  [3]: [CommonInfoModel, (r) => ({
-    formItems: [
-      {
-        label: '特色', 
-        name: 'advant', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入特色',
-          maxLength: 300,
-          showWordLimit: true, 
-        } as FieldProps,
-        rules:  [{
-          required: true,
-          message: '请输入特色',
-        }] 
-      },
-      {
-        label: '文化背景', 
-        name: 'intro', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入文化背景',
-          maxLength: 300,
-          showWordLimit: true, 
-        } as FieldProps,
-        rules:  [{
-          required: true,
-          message: '请输入文化背景',
-        }] 
-      },
-      {
-        label: '精彩推荐', 
-        name: 'recommend', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入精彩推荐',
-          maxLength: 300,
-          showWordLimit: true, 
-        } as FieldProps,
-        rules:  [{
-          required: true,
-          message: '请输入精彩推荐',
-        }] 
+export const villageInfoRouteForm : SingleForm = [CommonInfoModel, (r) => ({
+  formItems: [
+    {
+      label: '游览路线', 
+      name: 'serviceFacilities', 
+      type: 'flat-group', 
+      childrenColProps: {
+        span: 24,
       },
-      {
-        label: '推荐描述', 
-        name: 'reason', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入推荐描述',
-          maxLength: 300,
-          showWordLimit: true, 
-        } as FieldProps,
-        rules:  [{
-          required: true,
-          message: '请输入推荐描述',
-        }] 
+      children: [
+        {
+          label: '游览路线', 
+          name: 'route', 
+          type: 'text', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入游览路线',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入游览路线',
+          }] 
+        },
+        {
+          label: '路线名称', 
+          name: 'name', 
+          type: 'text', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入路线名称',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入路线名称',
+          }] 
+        },
+        {
+          label: '描述', 
+          name: 'desc', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入描述',
+            maxLength: 200,
+            showWordLimit: true, 
+          },
+          rules:  [{
+            required: true,
+            message: '请输入描述',
+          }] 
+        },
+        {
+          label: '起始点', 
+          name: 'startPoint', 
+          type: 'text', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入起始点',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入起始点',
+          }] 
+        },
+        {
+          label: '终止点', 
+          name: 'endPoint', 
+          type: 'text', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入终止点',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入终止点',
+          }] 
+        },
+        {
+          label: '预计时长', 
+          name: 'estimate', 
+          type: 'number', 
+          defaultValue: 1,
+          additionalProps: {
+            min: 1,
+            addonAfter: '小时',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入预计时长',
+          }] 
+        },
+        ...villageCommonContent(r, {
+          title: '游览路线',
+          showContent: false,
+          showTitle: false,
+        }).formItems
+      ]
+    },
+    
+    {
+      label: '活动时间', 
+      name: 'serviceFacilities', 
+      type: 'flat-group', 
+      childrenColProps: {
+        span: 24,
       },
-      {
-        label: '活动亮点', 
-        name: 'highlight', 
-        type: 'richtext', 
-        defaultValue: '',
-        additionalProps: {
-          placeholder: '请输入活动亮点',
-          maxLength: 300,
-          showWordLimit: true, 
-        } as FieldProps,
-        rules:  [{
-          required: true,
-          message: '请输入活动亮点',
-        }] 
+      children: [
+        {
+          label: '活动标题', 
+          name: 'activity', 
+          type: 'text', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入活动标题',
+          },
+          rules:  [{
+            required: true,
+            message: '请输入活动标题',
+          }] 
+        },
+        {
+          label: '活动开始时间', 
+          name: 'startTime',
+          type: 'datetime', 
+          defaultValue: '',
+          additionalProps: {
+            type: 'datetime',
+          },
+          rules: [{
+            required: true,
+            message: '请选择活动开始时间',
+          }],
+        },
+        {
+          label: '活动结束时间', 
+          name: 'endTime',
+          type: 'datetime', 
+          defaultValue: '',
+          additionalProps: {
+            type: 'datetime',
+          },
+          rules: [{
+            required: true,
+            message: '请选择活动结束时间',
+          }],
+        },
+        {
+          label: '活动时长', 
+          name: 'duration',
+          type: 'number', 
+          defaultValue: 0,
+          additionalProps: {
+            min: 0,
+            addonAfter: '分钟',
+          },
+          rules: [{
+            required: true,
+            message: '请选择活动时长',
+          }],
+        },
+        ...villageCommonContent(r, {
+          title: '活动时间',
+          showContent: false,
+          showTitle: false,
+        }).formItems
+      ] 
+    },
+    {
+      label: '路线特色', 
+      name: 'serviceFacilities', 
+      type: 'flat-group', 
+      childrenColProps: {
+        span: 24,
       },
-      ...villageCommonContent(r, {
-        title: '路线特色',
-        showContent: false,
-        showTitle: false,
-      }).formItems
-    ] 
-  })]
-}
+      children: [
+        {
+          label: '特色', 
+          name: 'advant', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入特色',
+            maxLength: 300,
+            showWordLimit: true, 
+          } as FieldProps,
+          rules:  [{
+            required: true,
+            message: '请输入特色',
+          }] 
+        },
+        {
+          label: '文化背景', 
+          name: 'intro', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入文化背景',
+            maxLength: 300,
+            showWordLimit: true, 
+          } as FieldProps,
+          rules:  [{
+            required: true,
+            message: '请输入文化背景',
+          }] 
+        },
+        {
+          label: '精彩推荐', 
+          name: 'recommend', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入精彩推荐',
+            maxLength: 300,
+            showWordLimit: true, 
+          } as FieldProps,
+          rules:  [{
+            required: true,
+            message: '请输入精彩推荐',
+          }] 
+        },
+        {
+          label: '推荐描述', 
+          name: 'reason', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入推荐描述',
+            maxLength: 300,
+            showWordLimit: true, 
+          } as FieldProps,
+          rules:  [{
+            required: true,
+            message: '请输入推荐描述',
+          }] 
+        },
+        {
+          label: '活动亮点', 
+          name: 'highlight', 
+          type: 'richtext', 
+          defaultValue: '',
+          additionalProps: {
+            placeholder: '请输入活动亮点',
+            maxLength: 300,
+            showWordLimit: true, 
+          } as FieldProps,
+          rules:  [{
+            required: true,
+            message: '请输入活动亮点',
+          }] 
+        },
+        ...villageCommonContent(r, {
+          title: '路线特色',
+          showContent: false,
+          showTitle: false,
+        }).formItems
+      ]
+    },
+  ] 
+}), { title: '旅游路线', typeName: '', }];

+ 42 - 18
src/pages/forms/forms.ts

@@ -1,4 +1,4 @@
-import { CommonInfoModel} from "@/api/inhert/VillageInfoApi";
+import { CommonInfoModel } from "@/api/inhert/VillageInfoApi";
 import type { IDynamicFormOptions, IDynamicFormRef } from "@imengyu/vue-dynamic-form";
 import type { NewDataModel } from "@imengyu/js-request-transform";
 import type { Ref } from "vue";
@@ -7,15 +7,18 @@ import { villageInfoBuildingForm, villageInfoDistributionForm } from "./data/bui
 import { villageInfoCulture, villageInfoFolkCultureForm } from "./data/cultural";
 import { villageInfoFoodProductsForm } from "./data/food";
 import { villageInfoOverviewForm } from "./data/overview";
-import { ichFormItems } from "./data/ich";
 import { villageInfoEnvironmentForm } from "./data/environment";
 import { villageInfoRelicForm } from "./data/relic";
 import { vilElementForm } from "./data/element";
 import { villageInfoFigureFormItems, villageInfoStoryFormItems } from "./data/history";
 import { villageInfoRouteForm, villageInfoTravelGuideForm } from "./data/travel";
 import { villageInfoSpeakerForm } from "./data/specker";
+import { ichFormItems } from "./data/ich";
 
-export type SingleForm = [NewDataModel, (formRef: Ref<IDynamicFormRef>) => IDynamicFormOptions]
+export type SingleForm = [NewDataModel, (formRef: Ref<IDynamicFormRef>) => IDynamicFormOptions, {
+  title: string,
+  typeName: string,
+}]
 export type GroupForm = Record<number, SingleForm>
 
 const villageInfoForm : Record<string, GroupForm> = {
@@ -41,12 +44,8 @@ const villageInfoForm : Record<string, GroupForm> = {
     [2]: villageInfoBuildingForm('历史建筑'),
     [3]: villageInfoBuildingForm('重要传统建筑'),
   },
-  'distribution': {
-    [0]: villageInfoDistributionForm,
-  },
-  'relic': {
-    [0]: villageInfoRelicForm,
-  },
+  'distribution': { [0]: villageInfoDistributionForm },
+  'relic': { [0]: villageInfoRelicForm },
   'folk_culture': {
     [1]: villageInfoFolkCultureForm('节庆活动'),
     [2]: villageInfoFolkCultureForm('祭祀崇礼'),
@@ -54,13 +53,9 @@ const villageInfoForm : Record<string, GroupForm> = {
     [4]: villageInfoFolkCultureForm('地方方言'),
     [5]: villageInfoFolkCultureForm('特色文化'),
   },
-  'ich': {
-    [0]: ichFormItems,
-  },
-  'travel_guide': {
-    [0]: villageInfoTravelGuideForm, 
-  },
-  'route': villageInfoRouteForm,
+  'ich': { [0]: ichFormItems },
+  'travel_guide': { [0]: villageInfoTravelGuideForm },
+  'route': { [0]: villageInfoRouteForm },
   'food_product': {
     [1]: villageInfoFoodProductsForm('农副产品'),
     [2]: villageInfoFoodProductsForm('食品产品'),
@@ -102,7 +97,7 @@ const villageInfoForm : Record<string, GroupForm> = {
           showTitle: false,
         }).formItems
       ]
-    })]
+    }), { title: '随手记', typeName: '', }]
   },
 }
 
@@ -110,5 +105,34 @@ export function getVillageInfoForm(subType: string, subId: number) {
   const group = villageInfoForm[subType];
   if (!group)
     throw new Error('subType ' + subType + ' not found');
+  if (subId === -1) 
+    return mergeFormItems(group);
   return group[subId];
-}
+}
+export function getVillageInfoFormIds(subType: string) {
+  return Object.keys(villageInfoForm[subType]).map((k) => Number(k));
+}
+
+export function mergeFormItems(set: Record<number, SingleForm>) {
+  const keys = Object.keys(set);
+  const result : SingleForm = [set[keys[0] as any][0], (formRef: Ref<IDynamicFormRef>) => {
+    const options : IDynamicFormOptions = {
+      formItems: [],
+    };
+    for (const key in set) {
+      const it = set[key];
+      const op = it[1](formRef);
+      const ex = it[2];
+      options.formItems.push({
+        label: ex.title, 
+        name: 'group' + key, 
+        type: 'flat-group', 
+        childrenColProps: { span: 24 },
+        children: op.formItems, 
+      });
+    }
+    return options;
+  }, set[keys[0] as any]?.[2] ?? { title: '合并', typeName: '' }]
+
+  return result;
+}

+ 120 - 0
src/pages/forms/task.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="tasks main-background main-background-type0">
+    <div class="nav-placeholder">
+    </div>
+    <section class="main-section">
+      <div class="content">
+        <a-button :icon="h(ArrowLeftOutlined)" class="mb-3" @click="router.back()">返回上一页</a-button>
+
+        <div class="d-flex flex-column" v-if="currentTaskDefItem" :padding="30" :gap="10">
+          <img 
+            v-if="currentTaskDefItem.banner" 
+            :src="currentTaskDefItem.banner"
+            mode="widthFix"
+          />
+          <TaskList>
+            <TaskItem 
+              v-for="item in currentTaskDefItem.list" 
+              :key="item.title" 
+              :icon="item.icon" 
+              :title="item.title"
+              :desc="item.desc" 
+              :enable="typeof item.enable === 'string' ? canCollect(item.enable) : item.enable"
+              @click="handleClick(item)" 
+            />
+          </TaskList>
+        </div>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, h } from 'vue';
+import { useRouter } from 'vue-router';
+import { useLoadQuerys } from '@imengyu/imengyu-web-shared';
+import { useCollectStore } from '@/stores/collect';
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+import { TaskMenuDef, type TaskMenuDefItem } from './tasks';
+import { ArrowLeftOutlined } from '@ant-design/icons-vue';
+import { getVillageInfoForm } from './forms';
+import VillageApi from '@/api/inhert/VillageApi';
+import TaskList from '../components/TaskList.vue';
+import TaskItem from '../components/TaskItem.vue';
+
+const router = useRouter();
+const { goForm } = useTaskEntryForm();
+const { canCollect, getCollectModuleInternalNameById } = useCollectStore();
+
+useLoadQuerys({
+  villageId: 0,
+  taskName: '',
+  taskPid: 0,
+}, async (querys) => {
+  const { villageId, taskName, taskPid } = querys;
+  currentTaskDefItem.value = { ...TaskMenuDef[taskName] };
+  if (taskPid) {
+    const res = (await VillageApi.getCatalogList(villageId, taskPid));
+    if (res.length === 0)
+      return;
+    currentTaskDefItem.value.list = res
+      .map(item => {
+        try {
+          const collectModuleInternalName = getCollectModuleInternalNameById(item.collectModuleId);
+          if (!collectModuleInternalName && item.collectModuleId)
+            throw new Error('不存在定义的表单数据');
+          const formDefine = collectModuleInternalName ? getVillageInfoForm(collectModuleInternalName, -1) : undefined;
+          return {
+            ...item,
+            enable: true,
+            catalogItem: item,
+            goForm: collectModuleInternalName ? [
+              collectModuleInternalName,
+              -1,
+              formDefine?.[2].typeName,
+              collectModuleInternalName === 'overview' ? 'common' : undefined,
+              item.title
+            ] : undefined,
+            onClick: () => {
+              if (item.haschild) {
+                router.push({
+                  path: '/forms/task', 
+                  query: {
+                    ...querys,
+                    taskName: item.title,
+                    taskPid: item.id,
+                  }
+                })
+              } else {
+                alert({
+                  title: item.title,
+                  content: '不存在定义的表单数据',
+                })
+              }
+            }
+          }
+        } catch (e) {
+          return {
+            ...item,
+            desc: '' + e,
+            enable: false,
+          }
+        }
+      });
+  }
+});
+
+const currentTaskDefItem = ref<TaskMenuDefItem | null>(null);
+const handleClick = (item: TaskMenuDefItem['list'][0]) => {
+  if (item.goForm) {
+    goForm(...item.goForm);
+    return;
+  }
+  if (item.onClick) {
+    item.onClick();
+  }
+}
+
+
+
+</script>

+ 375 - 0
src/pages/forms/tasks.ts

@@ -0,0 +1,375 @@
+export type TaskMenuDefItem = {
+  banner: string;
+  list: {
+    title: string;
+    desc: string;
+    icon: string;
+    enable: string|boolean;
+    goForm?: TaskMenuDefGoForm;
+    onClick?: () => void;
+  }[];
+}
+export type TaskMenuDefGoForm = [string, number, string|undefined, string|undefined, string|undefined];
+export type TaskRootMenuDefItem = {
+  title: string;
+  desc: string;
+  icon: string;
+  enable: string|boolean;
+  name: string;
+  goForm: {
+    title: string;
+    name: string;
+  }|TaskMenuDefGoForm
+}
+
+export const TaskRootDef : TaskRootMenuDefItem[] = [
+  {
+    title: '村落概况',
+    desc: '探索村落的历史渊源与发生轨迹',
+    icon: 'icon-task-summary',
+    enable: 'overview',
+    name: 'overview',
+    goForm: {
+      title: '村落概况',
+      name: 'overview',
+    }
+  },
+  {
+    title: '历史文化',
+    desc: '传承百年文化遗产和精神财富',
+    icon: 'icon-task-history',
+    enable: true,
+    name: 'history',
+    goForm: {
+      title: '历史文化',
+      name: 'history',
+    }
+  },
+  {
+    title: '非物质文化遗产项目',
+    desc: '维护文化多样性',
+    icon: 'icon-task-custom-1',
+    enable: 'ich',
+    name: 'ich',
+    goForm: [ 'ich', 0, undefined, undefined, '非物质文化遗产项目' ],
+  },
+  {
+    title: '环境格局',
+    desc: '感受自然人文环境之美',
+    icon: 'icon-task-environment',
+    enable: 'environment',
+    name: 'environment',
+    goForm: {
+      title: '环境格局',
+      name: 'environment',
+    }
+  },
+  {
+    title: '传统建筑',
+    desc: '领略古建筑的独特魅力',
+    icon: 'icon-task-building',
+    enable: true,
+    name: 'building',
+    goForm: {
+      title: '传统建筑',
+      name: 'building',
+    }
+  },
+  {
+    title: '民俗文化',
+    desc: '体验民间传统习俗与节庆',
+    icon: 'icon-task-custom',
+    enable: 'folk_culture',
+    name: 'folk_culture',
+    goForm: {
+      title: '民俗文化',
+      name: 'custom',
+    }
+  },
+  {
+    title: '地道美食',
+    desc: '正宗、传统地方特色美食',
+    icon: 'icon-task-food',
+    enable: 'food_product',
+    name: 'food',
+    goForm: {
+      title: '地道美食',
+      name: 'food',
+    }
+  },
+  {
+    title: '物产资源',
+    desc: '特定地域的植物、矿物或工艺品',
+    icon: 'icon-task-mine',
+    enable: 'food_product',
+    name: 'product',
+    goForm: {
+      title: '物产资源',
+      name: 'product',
+    }
+  },
+  {
+    title: '旅游路线',
+    desc: '体验独特的文化魅力',
+    icon: 'icon-task-trip',
+    enable: true,
+    name: 'trip',
+    goForm: {
+      title: '旅游路线',
+      name: 'trip',
+    }
+  }
+]
+
+export const TaskMenuDef : Record<string, TaskMenuDefItem> = {
+  'building': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_building.jpg',
+    list: [
+      {
+        title: '建筑分布',
+        desc: '村落内传统建筑分布情况',
+        icon: 'icon-task-building-1',
+        enable: 'distribution',
+        goForm: [ 'distribution', 0, undefined, undefined, '建筑分布' ],
+      },
+      {
+        title: '文物建筑',
+        desc: '历史、艺术、科学价值',
+        icon: 'icon-task-building-2',
+        enable: 'building',
+        goForm: [ 'building', 1, 'nature', undefined, '文物建筑' ],
+      },
+      {
+        title: '历史建筑',
+        desc: '重大历史事件记录',
+        icon: 'icon-task-building-3',
+        enable: 'building',
+        goForm: [ 'building', 2, 'nature', undefined, '历史建筑' ],
+      },
+      {
+        title: '重要传统建筑',
+        desc: '重要传统建筑的信息',
+        icon: 'icon-task-building-4',
+        enable: 'building',
+        goForm: [ 'building', 3, 'nature', undefined, '重要传统建筑' ],
+      },
+    ],
+  },
+  'custom': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_custom.jpg',
+    list: [
+      {
+        title: '节庆活动',
+        desc: '欢庆与传承并重的文化盛宴',
+        icon: 'icon-task-custom-2',
+        enable: 'folk_culture',
+        goForm: [ 'folk_culture', 1, 'folk_culture_type', undefined, '节庆活动' ],
+      },
+      {
+        title: '祭祀崇礼',
+        desc: '对先贤与自然的崇高致敬',
+        icon: 'icon-task-custom-3',
+        enable: 'folk_culture',
+        goForm: [ 'folk_culture', 2, 'folk_culture_type', undefined, '祭祀崇礼' ],
+      },
+      {
+        title: '婚丧嫁娶',
+        desc: '生命礼赞与文化传承的双重奏鸣',
+        icon: 'icon-task-custom-4',
+        enable: 'folk_culture',
+        goForm: [ 'folk_culture', 3, 'folk_culture_type', undefined, '婚丧嫁娶' ],
+      },
+      {
+        title: '地方方言',
+        desc: '历史沉淀的语言瑰宝',
+        icon: 'icon-task-custom-5',
+        enable: 'folk_culture',
+        goForm: [ 'folk_culture', 4, 'folk_culture_type', undefined, '地方方言' ],
+      },
+      {
+        title: '特色文化',
+        desc: '民族精神的鲜明烙印',
+        icon: 'icon-task-custom-6',
+        enable: 'folk_culture',
+        goForm: [ 'folk_culture', 5, 'folk_culture_type', undefined, '特色文化' ],
+      },
+    ],
+  },
+  'environment': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_environment.jpg',
+    list: [
+      {
+        title: '自然环境',
+        desc: '村落建立与发展历程',
+        icon: 'icon-task-environment-1',
+        enable: 'environment',
+        goForm: [ 'environment', 0, undefined, undefined, '自然环境' ],
+      },
+      {
+        title: '文物古迹',
+        desc: '重要历史文献资料',
+        icon: 'icon-task-environment-5',
+        enable: 'relic',
+        goForm: [ 'relic', 0, undefined, undefined, '文物古迹' ],
+      },
+      {
+        title: '历史环境要素',
+        desc: '村民口述历史记录',
+        icon: 'icon-task-environment-6',
+        enable: 'element',
+        goForm: [ 'element', 0, undefined, undefined, '历史环境要素' ],
+      },
+    ],
+  },
+  'food': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_food.jpg',
+    list: [
+      {
+        title: '农副产品',
+        desc: '乡村繁荣的多元支柱',
+        icon: 'icon-task-food-1',
+        enable: 'food_product',
+        goForm: [ 'food_product', 1, 'product_type', undefined, '农副产品' ],
+      },
+      {
+        title: '特色美食',
+        desc: '给味蕾探索带来无限惊喜',
+        icon: 'icon-task-food-2',
+        enable: 'food_product',
+        goForm: [ 'food_product', 3, 'product_type', undefined, '特色美食' ],
+      },
+    ],
+  },
+  'history': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_history.jpg',
+    list: [
+      {
+        title: '建村历史',
+        desc: '村落建立与发展历程',
+        icon: 'icon-task-history-1',
+        enable: 'cultural',
+        goForm: [ 'cultural', 1, 'cultural_type', undefined, '建村历史' ],
+      },
+      {
+        title: '历史人物',
+        desc: '重要历史人物事迹',
+        icon: 'icon-task-history-2',
+        enable: 'cultural',
+        goForm: [ 'figure', 0, undefined, undefined, '历史人物' ],
+      },
+      {
+        title: '历史事件',
+        desc: '重大历史事件记录',
+        icon: 'icon-task-history-3',
+        enable: 'cultural',
+        goForm: [ 'cultural', 2, 'cultural_type', undefined, '历史事件' ],
+      },
+      {
+        title: '掌故轶事',
+        desc: '民间传说与历史故事',
+        icon: 'icon-task-history-4',
+        enable: 'story',
+        goForm: [ 'story', 0, undefined, undefined, '掌故轶事' ],
+      },
+      {
+        title: '历史文献',
+        desc: '重要历史文献资料',
+        icon: 'icon-task-history-5',
+        enable: 'cultural',
+        goForm: [ 'cultural', 3, 'cultural_type', undefined, '历史文献' ],
+      },
+      {
+        title: '口述历史',
+        desc: '村民口述历史记录',
+        icon: 'icon-task-history-6',
+        enable: 'cultural',
+        goForm: [ 'cultural', 4, 'cultural_type', undefined, '口述历史' ],
+      },
+    ],
+  },
+  'product': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_mine.jpg',
+    list: [
+      {
+        title: '商业集市',
+        desc: '文化交流的开放窗口',
+        icon: 'icon-task-mine-1',
+        enable: 'food_product',
+        goForm: [ 'food_product', 4, 'product_type', undefined, '商业集市' ],
+      },
+      {
+        title: '服装服饰',
+        desc: '艺术与功能交织的时尚篇章',
+        icon: 'icon-task-mine-2',
+        enable: 'food_product',
+        goForm: [ 'food_product', 5, 'product_type', undefined, '服装服饰' ],
+      },
+      {
+        title: '运输工具',
+        desc: '历史的运输工具',
+        icon: 'icon-task-mine-2',
+        enable: 'food_product',
+        goForm: [ 'food_product', 6, 'product_type', undefined, '运输工具' ],
+      },
+    ],
+  },
+  'overview': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_summary.jpg',
+    list: [
+      {
+        title: '行政区划',
+        desc: '村落行政区域划分及变迁',
+        icon: 'icon-task-summary-1',
+        enable: 'overview',
+        goForm: [ 'overview', 1, undefined, 'common', '行政区划' ],
+      },
+      {
+        title: '村落综述',
+        desc: '村落整体概况介绍',
+        icon: 'icon-task-summary-5',
+        enable: 'overview',
+        goForm: [ 'overview', 5, undefined, 'common', '村落综述' ],
+      },
+      {
+        title: '地理信息',
+        desc: '地理位置和自然环境特征',
+        icon: 'icon-task-summary-2',
+        enable: 'overview',
+        goForm: [ 'overview', 2, undefined, 'common', '地理信息' ],
+      },
+      {
+        title: '建设与保护',
+        desc: '村落发展与文化遗产保护',
+        icon: 'icon-task-summary-3',
+        enable: 'overview',
+        goForm: [ 'overview', 3, undefined, 'common', '建设与保护' ],
+      },
+      {
+        title: '人口与经济',
+        desc: '人口与经济情况',
+        icon: 'icon-task-summary-4',
+        enable: 'overview',
+        goForm: [ 'overview', 4, undefined, 'common', '人口与经济' ],
+      },
+    ],
+  },
+  'trip': {
+    banner: 'https://mn.wenlvti.net/app_static/xiangan/banner_dig_trip.jpg',
+    list: [
+      {
+        title: '旅游导览',
+        desc: '',
+        icon: 'icon-task-trip-3',
+        enable: 'travel_guide',
+        goForm: [ 'travel_guide', 0, undefined, undefined, '旅游导览' ],
+      },
+      {
+        title: '旅游路线',
+        desc: '',
+        icon: 'icon-task-trip-1',
+        enable: 'route',
+        goForm: [ 'route', 0, undefined, undefined, '旅游路线' ],
+      },
+    ],
+  },
+}

+ 0 - 43
src/pages/task/building.vue

@@ -1,43 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_building.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="建筑分布" 
-      desc="村落内传统建筑分布情况" 
-      icon="icon-task-building-1" 
-      :enabled="canCollect('distribution')"
-      @click="goForm('distribution', 0)"
-    />
-    <TaskItem 
-      title="文物建筑" 
-      desc="历史、艺术、科学价值" 
-      icon="icon-task-building-2" 
-      :enabled="true"
-      @click="goForm('building', 1, 'nature')"
-    />
-    <TaskItem 
-      title="历史建筑" 
-      desc="重大历史事件记录" 
-      icon="icon-task-building-3" 
-      :enabled="true"
-      @click="goForm('building', 2, 'nature')"
-    />
-    <TaskItem 
-      title="重要传统建筑" 
-      desc="重要传统建筑的信息" 
-      icon="icon-task-building-4" 
-      :enabled="true"
-      @click="goForm('building', 3, 'nature')"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import { useCollectStore } from '@/stores/collect';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-const { canCollect } = useCollectStore();
-</script>

+ 0 - 50
src/pages/task/custom.vue

@@ -1,50 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_custom.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="节庆活动" 
-      desc="欢庆与传承并重的文化盛宴" 
-      icon="icon-task-custom-2" 
-      :enabled="true"
-      @click="goForm('folk_culture', 1, 'folk_culture_type')"
-    />
-    <TaskItem 
-      title="祭祀崇礼" 
-      desc="对先贤与自然的崇高致敬" 
-      icon="icon-task-custom-3" 
-      :enabled="true"
-      @click="goForm('folk_culture', 2, 'folk_culture_type')"
-    />
-    <TaskItem 
-      title="婚丧嫁娶" 
-      desc="生命礼赞与文化传承的双重奏鸣" 
-      icon="icon-task-custom-4" 
-      :enabled="true"
-      @click="goForm('folk_culture', 3, 'folk_culture_type')"
-    />
-    <TaskItem 
-      title="地方方言" 
-      desc="历史沉淀的语言瑰宝" 
-      icon="icon-task-custom-5" 
-      :enabled="true"
-      @click="goForm('folk_culture', 4, 'folk_culture_type')"
-    />
-    <TaskItem 
-      title="特色文化" 
-      desc="民族精神的鲜明烙印" 
-      icon="icon-task-custom-6" 
-      :enabled="true"
-      @click="goForm('folk_culture', 5, 'folk_culture_type')"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useCollectStore } from '@/stores/collect';
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-const { canCollect } = useCollectStore();
-</script>

+ 0 - 36
src/pages/task/environment.vue

@@ -1,36 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_environment.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="自然环境" 
-      desc="村落建立与发展历程" 
-      icon="icon-task-environment-1" 
-      :enabled="canCollect('environment')"
-      @click="goForm('environment', 0)"
-    />
-    <TaskItem 
-      title="文物古迹" 
-      desc="重要历史文献资料" 
-      icon="icon-task-environment-5" 
-      :enabled="canCollect('relic')"
-      @click="goForm('relic', 0)"
-    />
-    <TaskItem 
-      title="历史环境要素" 
-      desc="历史环境要素资料" 
-      icon="icon-task-environment-6" 
-      :enabled="canCollect('element')"
-      @click="goForm('element', 0)"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import { useCollectStore } from '@/stores/collect';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-const { canCollect } = useCollectStore();
-</script>

+ 0 - 27
src/pages/task/food.vue

@@ -1,27 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_food.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="农副产品" 
-      desc="乡村繁荣的多元支柱" 
-      icon="icon-task-food-1" 
-      :enabled="true"
-      @click="goForm('food_product', 1, 'food_product_type')"
-    />
-    <TaskItem 
-      title="特色美食" 
-      desc="给味蕾探索带来无限惊喜" 
-      icon="icon-task-food-2" 
-      :enabled="true"
-      @click="goForm('food_product', 3, 'product_type')"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-</script>

+ 0 - 57
src/pages/task/history.vue

@@ -1,57 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_history.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="建村历史" 
-      desc="村落建立与发展历程" 
-      icon="icon-task-history-1" 
-      :enabled="canCollect('cultural')"
-      @click="goForm('cultural', 1, 'cultural_type')"
-    />
-    <TaskItem 
-      title="历史人物" 
-      desc="重要历史人物事迹" 
-      icon="icon-task-history-2" 
-      :enabled="canCollect('figure')"
-      @click="goForm('figure', 0)"
-    />
-    <TaskItem 
-      title="历史事件" 
-      desc="重大历史事件记录" 
-      icon="icon-task-history-3" 
-      :enabled="canCollect('cultural')"
-      @click="goForm('cultural', 2, 'cultural_type')"
-    />
-    <TaskItem 
-      title="掌故轶事" 
-      desc="民间传说与历史故事" 
-      icon="icon-task-history-4" 
-      :enabled="canCollect('story')"
-      @click="goForm('story', 0)"
-    />
-    <TaskItem 
-      title="历史文献" 
-      desc="重要历史文献资料" 
-      icon="icon-task-history-5" 
-      :enabled="canCollect('cultural')"
-      @click="goForm('cultural', 3)"
-    />
-    <TaskItem 
-      title="口述历史" 
-      desc="村民口述历史记录" 
-      icon="icon-task-history-6" 
-      :enabled="canCollect('cultural')"
-      @click="goForm('cultural', 4)"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useCollectStore } from '@/stores/collect';
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-const { canCollect } = useCollectStore();
-</script>

+ 0 - 31
src/pages/task/index.vue

@@ -1,31 +0,0 @@
-<template>
-  <div class="tasks main-background main-background-type0">
-    <div class="nav-placeholder">
-    </div>
-    <section class="main-section">
-      <div class="content">
-        <a-button :icon="h(ArrowLeftOutlined)" class="mb-2" @click="handleBack">返回主页</a-button>
-        <router-view />
-      </div>
-    </section>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { h } from 'vue';
-import { ArrowLeftOutlined } from '@ant-design/icons-vue';
-import { useRouter } from 'vue-router';
-
-const router = useRouter();
-
-function handleBack() {
-  router.back()
-}
-</script>
-
-<style lang="scss">
-.tasks .head-img {
-  height: 260px;
-  object-fit: cover;
-}
-</style>

+ 0 - 34
src/pages/task/mine.vue

@@ -1,34 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_mine.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="商业集市" 
-      desc="文化交流的开放窗口" 
-      icon="icon-task-mine-1" 
-      :enabled="true"
-      @click="goForm('food_product', 4, 'product_type')"
-    />
-    <TaskItem 
-      title="服装服饰" 
-      desc="艺术与功能交织的时尚篇章" 
-      icon="icon-task-mine-2" 
-      :enabled="true"
-      @click="goForm('food_product', 5, 'product_type')"
-    />
-    <TaskItem 
-      title="运输工具" 
-      desc="历史的运输工具" 
-      icon="icon-task-mine-2" 
-      :enabled="true"
-      @click="goForm('food_product', 6, 'product_type')"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-</script>

+ 0 - 52
src/pages/task/summary.vue

@@ -1,52 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_summary.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="行政区划" 
-      desc="村落行政区域划分及变迁" 
-      icon="icon-task-summary-1" 
-      :enabled="true"
-      @click="goForm(1)"
-    />
-    <TaskItem 
-      title="村落综述" 
-      desc="村落整体概况介绍" 
-      icon="icon-task-summary-5" 
-      :enabled="true"
-      @click="goForm(5)"
-    />
-    <TaskItem 
-      title="地理信息" 
-      desc="地理位置和自然环境特征" 
-      icon="icon-task-summary-2" 
-      :enabled="true"
-      @click="goForm(2)"
-    />
-    <TaskItem 
-      title="建设与保护" 
-      desc="村落发展与文化遗产保护" 
-      icon="icon-task-summary-3" 
-      :enabled="true"
-      @click="goForm(3)"
-    />
-    <TaskItem 
-      title="人口与经济" 
-      desc="人口与经济情况" 
-      icon="icon-task-summary-4" 
-      :enabled="true"
-      @click="goForm(4)"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const t = useTaskEntryForm();
-
-function goForm(subId: number) {
-  t.goForm('overview', subId, undefined, 'common');
-}
-</script>

+ 0 - 53
src/pages/task/trip.vue

@@ -1,53 +0,0 @@
-<template>
-  <img class="head-img w-100 radius-base" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_trip.jpg" />
-  <TaskList>
-    <TaskItem 
-      title="旅游导览" 
-      desc="" 
-      icon="icon-task-trip-3" 
-      :enabled="canCollect('travel_guide')"
-      @click="goForm('travel_guide', 0)"
-    />
-    <TaskItem 
-      title="游览路线" 
-      desc="" 
-      icon="icon-task-trip-1" 
-      :enabled="canCollect('route')"
-      @click="goForm('route', 1)"
-    />
-    <TaskItem 
-      title="活动时间" 
-      desc="" 
-      icon="icon-task-trip-2" 
-      :enabled="canCollect('route')"
-      @click="goForm('route', 2)"
-    />
-    <TaskItem 
-      title="路线特色" 
-      desc="" 
-      icon="icon-task-trip-4" 
-      :enabled="canCollect('route')"
-      @click="goForm('route', 3)"
-    />
-  </TaskList>
-</template>
-
-<script setup lang="ts">
-import { useCollectStore } from '@/stores/collect';
-import { useTaskEntryForm } from '../composeable/TaskEntryForm';
-import TaskList from '../components/TaskList.vue';
-import TaskItem from '../components/TaskItem.vue';
-
-const { goForm } = useTaskEntryForm();
-const { canCollect } = useCollectStore();
-</script>
-
-<style lang="scss" scoped>
-.task-list {
-  .item{
-    .title{
-      margin-bottom: 0;
-    }
-  }
-}
-</style>

+ 5 - 47
src/router/index.ts

@@ -36,53 +36,6 @@ const router = createRouter({
       component: () => import('@/pages/details.vue'),
     },
     {
-      path: '/task',
-      name: 'Task',
-      component: () => import('@/pages/task/index.vue'),
-      children: [
-        {
-          path: 'mine',
-          name: 'Mine',
-          component: () => import('@/pages/task/mine.vue'),
-        },
-        {
-          path: 'history',
-          name: 'History',
-          component: () => import('@/pages/task/history.vue'),
-        },
-        {
-          path: 'food',
-          name: 'Food',
-          component: () => import('@/pages/task/food.vue'),
-        },
-        {
-          path: 'environment',
-          name: 'Environment',
-          component: () => import('@/pages/task/environment.vue'),
-        },
-        {
-          path: 'custom',
-          name: 'Custom',
-          component: () => import('@/pages/task/custom.vue'),
-        },
-        {
-          path: 'building',
-          name: 'Building',
-          component: () => import('@/pages/task/building.vue'),
-        },
-        {
-          path: 'summary',
-          name: 'Summary',
-          component: () => import('@/pages/task/summary.vue'),
-        },
-        {
-          path: 'trip',
-          name: 'Trip',
-          component: () => import('@/pages/task/trip.vue'),
-        },
-      ]
-    },
-    {
       path: '/forms/common',
       name: 'FormCommon',
       component: () => import('@/pages/forms/common.vue'),
@@ -93,6 +46,11 @@ const router = createRouter({
       component: () => import('@/pages/forms/list.vue'),
     },
     {
+      path: '/forms/task',
+      name: 'FormTask',
+      component: () => import('@/pages/forms/task.vue'),
+    },
+    {
       path: '/admin',
       name: 'Admin',
       component: () => import('@/pages/admin.vue'),

+ 13 - 0
src/stores/collect.ts

@@ -44,6 +44,18 @@ export const useCollectStore = defineStore('collect', () => {
       return collectableModules.value.get(CollectableModulesNameMapping[module]);
     return collectableModules.value.get(module);
   }
+  function getCollectModuleInternalNameById(id: number) {
+    for (const [key, value] of collectableModules.value) {
+      if (value == id) {
+        for (const k2 in CollectableModulesNameMapping) {
+          if (CollectableModulesNameMapping[k2] == key) {
+            return k2;
+          }
+        }
+      }
+    }
+    return '';
+  }
   
   const isEmpty = computed(() => collectableModules.value.size === 0);
 
@@ -51,6 +63,7 @@ export const useCollectStore = defineStore('collect', () => {
     isEmpty,
     collectableModules,
     setCollectableModules,
+    getCollectModuleInternalNameById,
     getCollectModuleId,
     canCollect,
   }