浏览代码

📦 基础表单组件

快乐的梦鱼 2 周之前
父节点
当前提交
f815fde5d7

+ 111 - 4
Untitled-1.txt

@@ -2,13 +2,120 @@
 本项目采用的是Vue3 web技术栈,其中主要的功能点为表单提交,因此本项目采用了动态表单库 vue-dynamic-form,
 关于动态表单库的使用文档可参考https://docs.imengyu.top/vue-dynamic-form-docs/guide/about.html
 
+可用组件:在 \src\components\dynamicf\index.ts 中的 registerAllFormComponents 函数中有注册了本项目中可用的动态表单组件,可以在这里查阅。
+register的一个参数小写名字为组件的唯一标识,在配置中使用。
+本项目大部分使用 ant-design-vue 的组件,少部分组件为二次封装组件,放在项目的 \src\components\dynamicf 文件夹下。
+
+可用组件简表:
+text:单行文本框
+password:密码输入框
+number:数字输入框
+text-area:多行文本框
+switch:开关
+check-box:boolean类型的勾选框
+check-box-int:0,1数字类型的勾选框
+rate:评星
+select:静态数据下拉选择框
+select-value:静态数据下拉选择框,额外参数的options可配置选项数据,格式{text: string,value: unknown}[]
+select-id: 动态数据据下拉选择框,额外参数的loadData为回调:
+  loadData: (searchText: string | null) => Promise<DropdownValues<T>[]>;
+  DropdownValues<T> {
+    label: string,
+    value: number,
+    raw: T;
+  }
+  可用来加载选项数据。
+date:日期选择.
+time:时间选择。
+date-time:日期+时间选择
+date-range:日期范围选择。
+time-range:时间范围选择。
+date-time-range:日期+时间范围选择。
+single-image:单一图片上传。
+mulit-image:多图上传。
 
+formOptions 格式如下:{
+  formLabelCol: { span: 6 }, //表单标签栅格宽度
+  formWrapperCol: { span: 24 }, //表单容器栅格宽度
+  formAdditionaProps: { //指定ant desgin form 的其他参数
+    layout: 'vertical'
+  },
+  formItems: [//表单项目
+    {
+      label: '传承人姓名', //标签名称
+      name: 'name', //字段名称
+      type: 'text', //组件名称
+      additionalProps: { //组件的额外参数
+        placeholder: '请输入姓名'
+      },
+    },
+    { 
+      label: '证件照',
+      name: 'idPhoto',
+      type: 'single-image',
+      additionalProps: {
+
+      },
+    },
+    {
+      label: '类型',
+      name: 'type',
+      type: 'select-id',//动态下拉加载
+      additionalProps: {
+        placeholder: '请选择类型',
+        //如有动态加载数据,请为我生成类似代码
+        loadData: async () =>
+          (await CommonContent.getCategoryList(4)).map(p => ({
+            label: p.title,
+            value: p.id,
+            raw: p
+          }))
+      } as IdAsValueDropdownProps<DataModel>,
+    },
+    {
+      label: '性别',
+      name: 'gender',
+      type: 'select',
+      additionalProps: {
+        options: [
+          { text: '男', value: '男' },
+          { text: '女', value: '女' },
+        ]
+      },
+    },
+    {
+      label: '生日',
+      name: 'birthday',
+      type: 'date',
+      additionalProps: {
+        placeholder: '请输入出生日期' 
+      }
+    },
+    {
+      label: '说明',
+      name: 'jobTitle',
+      type: 'text-area',
+      additionalProps: {
+        placeholder: '请输入说明' 
+      }
+    },
+  ],
+  formRules: { //表单验证项,格式与 async-validator 一致
+    name: [
+      { required: true, message: '请输入姓名' },
+      { min: 2, max: 5, message: '长度在 2 到 5 个字符' }
+    ],
+    ichName: [
+      { required: true, message: '请输入项目名称' }
+    ],
+    //....
+  },
+});
 
 你的主要任务是,为我生成重复的枯燥的表单配置:
 我会输入后端需要提交的字段列表,这包含:参数名、必选、类型、说明,
 为我选择最适合展现的表单组件,依据 vue-dynamic-form 动态表单库的配置与功能,
-为我攥写一个完整可用的表单配置,并写入页面的变量中。
+为我攥写一个完整可用的表单配置,并写入页面的变量中:
+变量已经写好, formModel为表单数据,已经写好。formOptions为表单配置,含有以下字段:formItems表单项目,formRules表单验证项。
+注:后端字段名称为下划线小写,前端需要修改为小驼峰写法。
 
-可用组件:在 \src\components\dynamicf\index.ts 中的 registerAllFormComponents 函数中有注册了本项目中可用的动态表单组件,可以在这里查阅。
-register的一个参数小写名字为组件的唯一标识,在配置中使用。
-本项目大部分使用 ant-design-vue 的组件,少部分组件为二次封装组件,放在项目的 \src\components\dynamicf 文件夹下。

+ 335 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "@imengyu/vue-dynamic-form": "^0.1.1",
         "@imengyu/vue-scroll-rect": "^0.1.3",
         "@vuemap/vue-amap": "^2.1.12",
+        "@vueup/vue-quill": "^1.2.0",
         "ant-design-vue": "^4.2.6",
         "axios": "^1.9.0",
         "bootstrap": "^5.3.0",
@@ -22,6 +23,7 @@
         "nprogress": "^0.2.0",
         "nuxt": "^3.17.6",
         "pinia": "^3.0.3",
+        "quill-image-uploader": "^1.3.0",
         "tslib": "^2.8.1",
         "vue": "^3.5.18",
         "vue-router": "^4.5.1",
@@ -4940,6 +4942,19 @@
         "vue": "3"
       }
     },
+    "node_modules/@vueup/vue-quill": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@vueup/vue-quill/-/vue-quill-1.2.0.tgz",
+      "integrity": "sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==",
+      "license": "MIT",
+      "dependencies": {
+        "quill": "^1.3.7",
+        "quill-delta": "^4.2.2"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.41"
+      }
+    },
     "node_modules/@whatwg-node/disposablestack": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
@@ -5686,6 +5701,24 @@
         "node": ">=8"
       }
     },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/call-bind-apply-helpers": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -5939,6 +5972,15 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cluster-key-slot": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -6517,6 +6559,26 @@
         "callsite": "^1.0.0"
       }
     },
+    "node_modules/deep-equal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
+      "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arguments": "^1.1.1",
+        "is-date-object": "^1.0.5",
+        "is-regex": "^1.1.4",
+        "object-is": "^1.1.5",
+        "object-keys": "^1.1.1",
+        "regexp.prototype.flags": "^1.5.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/deepmerge": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -6567,6 +6629,23 @@
         "node": ">= 0.10.0"
       }
     },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/define-lazy-prop": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@@ -6576,6 +6655,23 @@
         "node": ">=8"
       }
     },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/defu": {
       "version": "6.1.4",
       "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
@@ -7254,6 +7350,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/eventemitter3": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+      "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
+      "license": "MIT"
+    },
     "node_modules/events": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7304,6 +7406,12 @@
       "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
       "license": "MIT"
     },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
     "node_modules/extend-shallow": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@@ -7370,6 +7478,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "license": "Apache-2.0"
+    },
     "node_modules/fast-fifo": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -7668,6 +7782,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/fuse.js": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
@@ -7962,6 +8085,18 @@
       "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
       "license": "MIT"
     },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/has-symbols": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -8244,6 +8379,22 @@
         "url": "https://github.com/sponsors/brc-dd"
       }
     },
+    "node_modules/is-arguments": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+      "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-arrayish": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -8293,6 +8444,22 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-date-object": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-docker": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@@ -8436,6 +8603,24 @@
         "@types/estree": "*"
       }
     },
+    "node_modules/is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-ssh": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz",
@@ -8867,6 +9052,12 @@
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
       "license": "MIT"
     },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+      "license": "MIT"
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -8885,6 +9076,13 @@
       "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
       "license": "MIT"
     },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+      "license": "MIT"
+    },
     "node_modules/lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -9949,6 +10147,31 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/ofetch": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
@@ -10308,6 +10531,12 @@
       "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==",
       "license": "MIT"
     },
+    "node_modules/parchment": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
+      "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/parse-gitignore": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz",
@@ -11225,6 +11454,60 @@
       ],
       "license": "MIT"
     },
+    "node_modules/quill": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
+      "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "clone": "^2.1.1",
+        "deep-equal": "^1.0.1",
+        "eventemitter3": "^2.0.3",
+        "extend": "^3.0.2",
+        "parchment": "^1.1.4",
+        "quill-delta": "^3.6.2"
+      }
+    },
+    "node_modules/quill-delta": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz",
+      "integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-diff": "1.2.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.isequal": "^4.5.0"
+      }
+    },
+    "node_modules/quill-image-uploader": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/quill-image-uploader/-/quill-image-uploader-1.3.0.tgz",
+      "integrity": "sha512-vO43GEn93rGThje/MlotkQE9OV5nOKBZ4oKhn71L/EjrM/J2P/8iDDVd9GEwlsGsbskeJqPLopsSQ4HlVVIn6A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "quill": "^1.3.7"
+      }
+    },
+    "node_modules/quill/node_modules/fast-diff": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+      "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/quill/node_modules/quill-delta": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
+      "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
+      "license": "MIT",
+      "dependencies": {
+        "deep-equal": "^1.0.1",
+        "extend": "^3.0.2",
+        "fast-diff": "1.1.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/quote-unquote": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz",
@@ -11419,6 +11702,26 @@
         "regexp-tree": "bin/regexp-tree"
       }
     },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/remove-trailing-separator": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -11772,6 +12075,38 @@
         "node": ">= 18"
       }
     },
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",

+ 2 - 0
package.json

@@ -19,6 +19,7 @@
     "@imengyu/vue-dynamic-form": "^0.1.1",
     "@imengyu/vue-scroll-rect": "^0.1.3",
     "@vuemap/vue-amap": "^2.1.12",
+    "@vueup/vue-quill": "^1.2.0",
     "ant-design-vue": "^4.2.6",
     "axios": "^1.9.0",
     "bootstrap": "^5.3.0",
@@ -28,6 +29,7 @@
     "nprogress": "^0.2.0",
     "nuxt": "^3.17.6",
     "pinia": "^3.0.3",
+    "quill-image-uploader": "^1.3.0",
     "tslib": "^2.8.1",
     "vue": "^3.5.18",
     "vue-router": "^4.5.1",

+ 57 - 0
src/api/CommonContent.ts

@@ -409,6 +409,63 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
       .then(res => res.data as T)
       .catch(e => { throw e });
   }
+
+  
+  /**
+   * 上传文件到服务器
+   */
+  async uploadSmallFile(
+    file: File, 
+    fileType?: "image" | "video" | "audio" | undefined, 
+    name = 'file', 
+    data?: Record<string, any>
+  ) {
+    return new Promise<{
+      fullurl: string,
+      url: string
+    }>(async (resolve, reject) => {
+      try {
+        let url = this.config.baseUrl + '/common/upload';
+        const formData = new FormData();
+        formData.append(name, file);
+
+        // 添加额外数据
+        if (data) {
+          Object.entries(data).forEach(([key, value]) => {
+            formData.append(key, value);
+          });
+        }
+
+        let requestOptions: RequestInit = {
+          method: 'POST',
+          body: formData,
+          headers: {}
+        };
+
+        // 应用请求拦截器
+        if (this.config.requestInceptor) {
+          const { newReq, newUrl } = this.config.requestInceptor(url, requestOptions as any);
+          url = newUrl;
+          requestOptions = newReq as RequestInit;
+        }
+
+        // 移除Content-Type,让浏览器自动处理
+        if (requestOptions.headers && (requestOptions.headers as Record<string, string>)['Content-Type'])
+          delete (requestOptions.headers as Record<string, string>)['Content-Type'];
+
+        const response = await fetch(url, requestOptions);
+        const responseData = await response.json();
+
+        if (!response.ok)
+          throw new Error(`HTTP error! status: ${response.status}`);
+        if (responseData.code !== 1)
+          throw new Error(responseData.msg ?? `code: ${responseData.code}`);
+        resolve(responseData.data);
+      } catch (error) {
+        reject(error);
+      }
+    });
+  }
 }
 
 export default new CommonContentApi(0, '默认通用内容');

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

@@ -63,6 +63,8 @@ export class UserApi extends AppServerRequestModule<DataModel> {
   async refresh() {
     return (await this.post('/ich/inheritor/refresh', {}, '刷新token', undefined, LoginResult)).data as LoginResult;
   }
+
+  
 }
 
 export default new UserApi();

+ 42 - 13
src/api/inheritor/InheritorContent.ts

@@ -7,13 +7,32 @@ export class IchInfo extends DataModel<IchInfo> {
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      lonlat: { serverSide: 'undefined' },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      typicalImages: { 
+        clientSide: 'object', 
+        clientSideChildDataModel: {
+          convertTable: {},
+        }, 
+        serverSide: 'string' 
+      },
+    };
+    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)[];
+
   id = 0 as number;
   mainBodyColumnId = 0 as number;
   title = '' as string;
-  region = 0 as number;
+  region = null as number|null;
   image = '' as string;
   imageDesc = '' as string|null;
   images = [] as string[];
@@ -27,10 +46,10 @@ export class IchInfo extends DataModel<IchInfo> {
   ztImage = '' as string|null;
   intro = '' as string;
   description = '' as string;
-  heritage = 0 as number;
-  level = 0 as number;
-  ichType = 0 as number;
-  batch = 0 as number;
+  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;
@@ -39,8 +58,13 @@ export class IchInfo extends DataModel<IchInfo> {
   address = '' as string|null;
   declarationRegion = '' as string;
   popularRegion = '' as string;
-  approveTime = 0 as number;
-  typicalImages = [] as string[];
+  approveTime = '' as string;
+  typicalImages = [] as {
+    form: string,
+    mobile: string,
+    desc: string,
+    url: string,
+  }[];
   thumbnail = '' as string;
   flagText = '' as string;
   typeText = '' as string;
@@ -66,7 +90,7 @@ export class InheritorInfo extends DataModel<InheritorInfo> {
   id = 0 as number;
   mainBodyColumnId = 0 as number;
   title = '' as string;
-  region = 0 as number;
+  region = null as number|null;
   image = '' as string;
   imageDesc = '' as string|null;
   images = [] as string[];
@@ -75,7 +99,7 @@ export class InheritorInfo extends DataModel<InheritorInfo> {
   flag = '' as string;
   keywords = '' as string|null;
   tags = '' as string;
-  associationId = 0 as number;
+  associationId = null as number|null;
   pid = 0 as number;
   alsoName = '' as string|null;
   nation = '' as string;
@@ -85,9 +109,9 @@ export class InheritorInfo extends DataModel<InheritorInfo> {
   content = '' as string|null;
   intro = '' as string;
   prize = '' as string;
-  level = 0 as number;
+  level = null as number|null;
   gender = 0 as number;
-  batch = 0 as number;
+  batch = '' as string|null;
   typicalImages = [] as string[];
   progress = 0 as number;
   contentId = 0 as number;
@@ -117,7 +141,7 @@ export class SeminarInfo extends DataModel<IchInfo> {
   id = 0 as number;
   mainBodyColumnId = 0 as number;
   title = '' as string;
-  region = 0 as number;
+  region = null as number|null;
   image = '' as string|null;
   imageDesc = '' as string|null;
   images = [] as string[];
@@ -135,7 +159,7 @@ export class SeminarInfo extends DataModel<IchInfo> {
   latitude = '' as string|null;
   address = '' as string;
 
-  featuresType = 0 as number;
+  featuresType = null as number|null;
   contact = '' as string;
   ichSiteType = '' as string;
   flagText = '' as string;
@@ -157,11 +181,16 @@ export class InheritorContentApi extends AppServerRequestModule<DataModel> {
   constructor() {
     super();
   }
+
   async getBaseInfo<T extends DataModel>(modelId: number, newDataModel: new () => T) {
     return (await this.post('/ich/inheritor/baseInfo', {
       model_id: modelId,
     }, '基础表信息', undefined, newDataModel)).data as T;
   }
+  async saveBaseInfo<T extends DataModel>(dataModel: T) {
+    return (await this.post('/ich/inheritor/saveBase', dataModel.toServerSide(), '基础内容表采集(非遗,传承人,传习所)'));
+  }
+
 
   async getIchInfo() {
     return await this.getBaseInfo(2, IchInfo);

+ 11 - 0
src/assets/scss/fix.scss

@@ -5,4 +5,15 @@
   --vc-pgn-background-color: #cfcfcfc4;
   --vc-clr-white: #333333;
   --vc-pgn-active-color: var(--vc-clr-primary)
+}
+.dynamic-form-group {
+  padding: 40px;
+  background-color: rgba(#eee, 0.6);
+  border-radius: 5px;
+}
+
+@media screen and (max-width: 768px) {
+  .dynamic-form-group {
+    padding: 20px;
+  }
 }

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

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

+ 37 - 0
src/components/dynamicf/Editor/QuillEditorWrapper.vue

@@ -0,0 +1,37 @@
+<template>
+  <QuillEditor
+    :modules="modules" 
+    toolbar="full" 
+    theme="snow"
+    contentType="html"
+    v-bind="$attrs"
+    :content="props.modelValue"
+    @update:content="(val: string) => emit('update:modelValue', val)"
+  />
+</template>
+
+<script lang="ts" setup>
+import { QuillEditor } from '@vueup/vue-quill'
+import '@vueup/vue-quill/dist/vue-quill.snow.css'
+import ImageUploader from 'quill-image-uploader';
+import CommonContent from '@/api/CommonContent';
+
+const emit = defineEmits(['update:modelValue'])
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  }
+})
+
+const modules = [{
+  name: 'imageUploader',
+  module: ImageUploader,
+  options: {
+    upload: async (file: any) => {
+      const { url } = await CommonContent.uploadSmallFile(file);
+      return url;
+    }
+  }
+}];
+</script>

二进制
src/components/dynamicf/Map/Maker.png


+ 76 - 0
src/components/dynamicf/Map/MapPointPicker.vue

@@ -0,0 +1,76 @@
+<template>
+  <div :style="{ width, height }">
+    <img src="./Maker.png" />
+    <span class="lonlat">{{ center[0] }}, {{ center[1] }}</span>
+    <el-amap
+      style="width: 100%"
+      v-model:center="center"
+      :zoom="zoom"
+      @init="handleInit"
+      v-bind="$attrs"
+    >
+    </el-amap>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, type PropType } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object as PropType<(number|string)[]>,
+    default: () => ([121.59996, 31.197646])
+  },
+  zoom: {
+    type: Number,
+    default: 12
+  },
+  width: {
+    type: [Number, String],
+    default: '100%'
+  },
+  height: {
+    type: [Number, String],
+    default: '300px'
+  }
+});
+
+const emit = defineEmits(['update:modelValue' ])
+const center = ref(props.modelValue);
+let map: any = null;
+
+function handleInit(mapRef: any) {
+  map = mapRef;
+}
+
+watch(center, (newVal) => {
+  emit('update:modelValue', newVal);
+})
+
+</script>
+
+<style scoped>
+div {
+  position: relative;
+}
+.lonlat {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  font-size: 12px;
+  padding: 10px;
+  color: #333;
+  background-color: #fff;
+  z-index: 100;
+}
+img {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 30px;
+  height: 30px;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

+ 5 - 0
src/components/dynamicf/index.ts

@@ -26,6 +26,9 @@ import CascaderFormItemVue from "./CascaderFormItem.vue";
 import SimpleEditDynamicStringListVue from "./SimpleEditDynamicStringList.vue";
 import WhiteSpaceVue from "./WhiteSpace.vue";
 import NumberRange from "./NumberRange.vue";
+import MapPointPicker from "./Map/MapPointPicker.vue";
+import { QuillEditor } from "@vueup/vue-quill";
+import QuillEditorWrapper from "./Editor/QuillEditorWrapper.vue";
 
 export const defaultConfig = {
   internalWidgets: {
@@ -88,4 +91,6 @@ export function registerAllFormComponents() {
     .register('static-date', markRaw(ShowDateOrNullVue))
     .register('static-image-list', markRaw(ShowImageListVue), {}, "images")
     .register('space', markRaw(WhiteSpaceVue))
+    .register('map-pick-point', markRaw(MapPointPicker))
+    .register('richtext', markRaw(QuillEditorWrapper), {}, 'modelValue')
 }

+ 4 - 0
src/main.ts

@@ -2,7 +2,9 @@ import 'vue3-carousel/carousel.css'
 import 'ant-design-vue/dist/reset.css';
 import '@vuemap/vue-amap/dist/style.css'
 import 'nprogress/nprogress.css';
+import '@imengyu/vue-dynamic-form/dist/style.css'
 import '@imengyu/vue-scroll-rect/lib/vue-scroll-rect.css'
+import '@vueup/vue-quill/dist/vue-quill.snow.css';
 
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
@@ -14,6 +16,7 @@ import { registryConvert } from '@/common/ConvertRgeistry'
 import VueAMap, {initAMapApiLoader} from '@vuemap/vue-amap';
 import NProgress from 'nprogress';
 import ScrollRect from '@imengyu/vue-scroll-rect'
+import { QuillEditor } from '@vueup/vue-quill'
 import { registerAllFormComponents } from './components/dynamicf';
 
 initAMapApiLoader({
@@ -34,6 +37,7 @@ app.use(createPinia())
 app.use(router)
 app.use(Antd)
 app.use(VueAMap)
+app.component('QuillEditor', QuillEditor);
 app.use(ScrollRect)
 
 app.mount('#app').$nextTick(() => {

+ 0 - 182
src/pages/form.vue

@@ -1,182 +0,0 @@
-<template>
-  <!-- 项目申报 -->
-  <div class="about main-background main-background-type0">
-    <div class="nav-placeholder"></div>
-    <!-- 表单 -->
-    <section class="main-section ">
-      <div class="content">
-        <div class="title">
-          <h2>项目申报</h2>
-        </div>
-        <DynamicForm 
-          ref="form"
-          :model="formModel" 
-          :options="formOptions"
-        />
-        <a-button type="primary" block @click="handleSubmit">提交</a-button>
-      </div>
-    </section>
-  </div>
-</template>
-
-<script setup lang="ts">
-import CommonContent from '@/api/CommonContent';
-import type { IdAsValueDropdownProps } from '@/components/dynamicf/Dropdown/IdAsValueDropdown';
-import type { DataModel } from '@imengyu/js-request-transform';
-import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
-import { Modal, type FormInstance } from 'ant-design-vue';
-import { ref } from 'vue';
-
-const form = ref<IDynamicFormRef>();
-const formModel = ref({});
-const formOptions = ref<IDynamicFormOptions>({
-  formLabelCol: { span: 6 },
-  formWrapperCol: { span: 24 },
-  formAdditionaProps: {
-    layout: 'vertical'
-  },
-  formItems: [
-    { 
-      label: '证件照', 
-      name: 'idPhoto',
-      type: 'single-image',
-      additionalProps: {
-
-      },
-    },
-    {
-      label: '传承人姓名',
-      name: 'name',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入姓名'
-      },
-    },
-    {
-      label: '项目名称',
-      name: 'ichName',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入项目名称' 
-      }
-    },
-    {
-      label: '类型',
-      name: 'type',
-      type: 'select-id',
-      additionalProps: {
-        placeholder: '请选择类型',
-        loadData: async () =>
-          (await CommonContent.getCategoryList(4)).map(p => ({
-            label: p.title,
-            value: p.id,
-            raw: p
-          }))
-      } as IdAsValueDropdownProps<DataModel>,
-    },
-    {
-      label: '保护单位',
-      name: 'unit',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入保护单位' 
-      }
-    },
-    {
-      label: '性别',
-      name: 'gender',
-      type: 'select',
-      additionalProps: {
-        options: [
-          { text: '男', value: '男' },
-          { text: '女', value: '女' },
-        ]
-      },
-    },
-    {
-      label: '生日',
-      name: 'birthday',
-      type: 'date',
-      additionalProps: {
-        placeholder: '请输入出生日期' 
-      }
-    },
-    {
-      label: '民族',
-      name: 'nation',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入民族' 
-      }
-    },
-    {
-      label: '传承人姓名',
-      name: 'name',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入姓名' 
-      }
-    },
-    {
-      label: '职业',
-      name: 'job',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入职业' 
-      }
-    },
-    {
-      label: '职务职称',
-      name: 'jobTitle',
-      type: 'text',
-      additionalProps: {
-        placeholder: '请输入职务职称' 
-      }
-    },
-  ],
-  formRules: {
-    name: [
-      { required: true, message: '请输入姓名' },
-      { min: 2, max: 5, message: '长度在 2 到 5 个字符' }
-    ],
-    // idPhoto: [
-    //   { required: true, message: '请上传证件照' }
-    // ],
-    ichName: [
-      { required: true, message: '请输入项目名称' }
-    ],
-    type: [
-      { required: true, message: '请选择类型' }
-    ],
-    unit: [
-      { required: true, message: '请输入保护单位' }
-    ],
-    gender: [
-      { required: true, message: '请选择性别' }
-    ],
-    birthday: [
-      { required: true, message: '请输入出生日期' }
-    ],
-    job: [
-      { required: true, message: '请输入职业' }
-    ],
-    jobTitle: [
-      { required: true, message: '请输入职业' } 
-    ],
-    nation: [
-      { required: true, message: '请输入民族' }
-    ]
-  },
-
-});
-
-function handleSubmit() {
-  (form.value?.getFormRef() as FormInstance).validate().then(() => { 
-    Modal.success({
-      title: '提交成功',
-      content: '您的项目申报已提交成功。请耐心等待审核!',
-    });
-  });
-}
-</script>
-

+ 110 - 0
src/pages/forms/form.vue

@@ -0,0 +1,110 @@
+<template>
+  <!-- 表单 -->
+  <div class="about main-background main-background-type0">
+    <div class="nav-placeholder"></div>
+    <!-- 表单 -->
+    <section class="main-section ">
+      <div class="content">
+        <div class="title">
+          <h2>{{ title }}</h2>
+        </div>
+        <a-spin v-if="loadingData" />
+        <template v-else>
+          <DynamicForm
+            ref="form"
+            :model="(formModel as any)" 
+            :options="formOptions"
+          />
+          <a-button 
+            type="primary"
+            block 
+            :loading="loading" class="mt-3" 
+            @click="handleSubmit"
+          >
+            提交
+          </a-button>
+        </template>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script setup lang="ts" generic="T extends DataModel">
+import InheritorContent from '@/api/inheritor/InheritorContent';
+import type { DataModel } from '@imengyu/js-request-transform';
+import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
+import { Modal, type FormInstance } from 'ant-design-vue';
+import { onMounted, ref, toRefs, type PropType } from 'vue';
+import { useRouter } from 'vue-router';
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: '项目申报'
+  },
+  formModel: {
+    type: Object as PropType<T>,
+    required: true
+  },
+  formOptions: {
+    type: Object as PropType<IDynamicFormOptions>,
+    required: true
+  },
+  load: {
+    type: Function as PropType<() => Promise<T>>,
+    default: () => Promise.resolve()
+  }
+})
+
+const { formModel, formOptions, load } = toRefs(props);
+const form = ref<IDynamicFormRef>();
+
+const router = useRouter();
+const loading = ref(false);
+const loadingData = ref(false);
+
+async function handleSubmit() {
+  loading.value = true;
+  try {
+    await (form.value?.getFormRef() as FormInstance).validate();
+  } catch {
+    loading.value = false;
+    return;
+  }
+  try {
+    await InheritorContent.saveBaseInfo(formModel.value);
+    Modal.success({
+      title: '提交成功',
+      content: '您的项目申报已提交成功。请耐心等待审核!',
+      onOk() {
+        router.push({ path: '/' })
+      },
+    });
+  } catch (error) {
+    Modal.error({
+      title: '提交失败',
+      content: '' + error,
+    });
+  } finally {
+    loading.value = false;
+  }
+}
+async function loadData() {
+  loadingData.value = true;
+  try {
+    await load.value();
+  } catch (error) {
+    Modal.error({
+      title: '加载失败',
+      content: '' + error,
+    });
+  } finally {
+    loadingData.value = false;
+  }
+}
+
+onMounted(async () => {
+  await loadData();
+})
+</script>
+

+ 205 - 0
src/pages/forms/ich.vue

@@ -0,0 +1,205 @@
+<template>
+  <!-- 项目申报 -->
+  
+  <Form 
+    :formModel="formModel"
+    :formOptions="formOptions"
+    :load="loadData"
+  />
+</template>
+
+<script setup lang="ts">
+import type { IDynamicFormOptions } from '@imengyu/vue-dynamic-form';
+import Form from './form.vue';
+import { onMounted, ref, type Ref } from 'vue';
+import InheritorContent, { IchInfo } from '@/api/inheritor/InheritorContent';
+import CommonContent from '@/api/CommonContent';
+import type { SelectProps } from 'ant-design-vue';
+import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
+import type { UploadImageFormItemProps } from '@/components/dynamicf/UploadImageFormItem';
+
+const formModel = ref(new IchInfo()) as Ref<IchInfo>;
+const formOptions = ref<IDynamicFormOptions>({
+  formLabelCol: { span: 6 },
+  formWrapperCol: { span: 24 },
+  formAdditionaProps: {
+    layout: 'vertical'
+  },
+  formItems: [
+    {
+      type: 'group-flat', 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 => ({ label: p.title, value: p.id, raw: p })) 
+          },  
+        },
+        { 
+          label: '非遗类型', name: 'ichType', type: 'select-id', 
+          additionalProps: { 
+            placeholder: '请选择非遗类型', 
+            loadData: async () => (await CommonContent.getCategoryList(4)).map(p => ({ label: p.title, value: p.id, raw: p })) 
+          },
+        },
+        { 
+          label: '批次', name: 'batch', type: 'select-id', 
+          additionalProps: { 
+            placeholder: '请选择批次', 
+            loadData: async () => (await CommonContent.getCategoryList(289)).map(p => ({ label: p.title, value: p.id, raw: p })) 
+          },
+        },
+        { label: '简介', name: 'intro', type: 'richtext', additionalProps: { placeholder: '请输入简介' } },
+        { label: '项目描述', name: 'description', type: 'richtext', additionalProps: { placeholder: '请输入项目描述' } },
+        { label: '传承值', name: 'heritage', type: 'text', additionalProps: { placeholder: '请输入传承值' } },
+        
+        { label: '地图坐标', name: 'lonlat', type: 'map-pick-point' },
+        
+        {
+          type: 'simple-flat', label: '', name: 'map',
+          childrenColProps: { span: 12 },
+          children: [
+            { label: '平面坐标X', name: 'mapX', type: 'number', additionalProps: { placeholder: '请输入平面坐标X' } },
+            { label: '平面坐标Y', name: 'mapY', type: 'number', additionalProps: { placeholder: '请输入平面坐标Y' } },
+          ]
+        },
+        { label: '保护单位', name: 'unit', type: 'text', additionalProps: { placeholder: '请输入保护单位' } },
+        { label: '地址', name: 'address', type: 'text', additionalProps: { placeholder: '请输入地址' } },
+        { label: '非遗编号', name: 'code', type: 'text', additionalProps: { placeholder: '请输入非遗编号' } },
+        { label: '申报地区', name: 'declarationRegion', type: 'text', additionalProps: { placeholder: '请输入申报地区' } },
+        { label: '流行地区', name: 'popularRegion', type: 'text', additionalProps: { placeholder: '请输入流行地区' } },
+        { label: '批准时间', name: 'approveTime', type: 'text', additionalProps: { placeholder: '请输入批准时间' } },
+        { 
+          label: '代表性图片', name: 'typicalImages', type: 'mulit-image', 
+          additionalProps: { 
+            placeholder: '请上传代表性图片', 
+            tip: '格式要求: JSON数组,包含from、mobile、desc、url字段' 
+          } 
+        },
+        { 
+          label: '展厅图片', name: 'ztImage', type: 'single-image', 
+          additionalProps: { 
+            placeholder: '请上传展厅图片',
+            uploadCo: useImageSimpleUploadCo(),
+          } as UploadImageFormItemProps,
+        },
+      ]
+    },
+    {
+      type: 'group-flat', label: '通用信息', name: 'commonInfo',
+      childrenColProps: { span: 24 },
+      children: [
+        { 
+          label: '地区', name: 'region', type: 'select-id', 
+          additionalProps: { 
+            placeholder: '请选择地区', 
+            loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ label: p.title, value: p.id, raw: p })) ,  
+          },
+        },
+        { 
+          label: '类型', name: 'type', type: 'select', 
+          additionalProps: { 
+            placeholder: '请选择类型', 
+            options: [
+              { text: '文章', value: 1 }, 
+              { text: '音频', value: 2 }, 
+              { text: '视频', value: 3 }, 
+              { text: '相册', value: 4 }, 
+              { text: '数字档案', value: 5 }] 
+            },  
+        },
+        { 
+          label: '图片', name: 'image', type: 'single-image', 
+          additionalProps: { 
+            placeholder: '请上传图片',
+            uploadCo: useImageSimpleUploadCo()
+          } as UploadImageFormItemProps, 
+        },
+        { 
+          label: '图片说明', name: 'imageDesc', type: 'text', 
+          additionalProps: { placeholder: '请输入图片说明' } 
+        },
+        { 
+          label: '转自', name: 'from', type: 'text', 
+          additionalProps: { placeholder: '请输入来源' }, 
+        },
+        { 
+          label: '组图', name: 'images', type: 'mulit-image', 
+          hidden: { callback: (_, model) => (model as IchInfo).type !== 4 },
+          additionalProps: { 
+            placeholder: '请上传图片',
+            maxCount: 20,
+            uploadCo: useImageSimpleUploadCo(),
+          } as UploadImageFormItemProps, 
+        },
+        { 
+          label: '音频', name: 'audio', type: 'text', 
+          hidden: { callback: (_, model) => (model as IchInfo).type !== 2 },
+          additionalProps: { placeholder: '请上传音频' }, 
+        },
+        { 
+          label: '视频', name: 'video', type: 'text', 
+          hidden: { callback: (_, model) => (model as IchInfo).type !== 3 },
+          additionalProps: { placeholder: '请上传视频' }, 
+        },
+        { 
+          label: '数字档案', name: 'archives', type: 'text', 
+          hidden: { callback: (_, model) => (model as IchInfo).type !== 5 },
+          additionalProps: { placeholder: '请上传数字档案' }, 
+        },
+        { 
+          label: '标志', name: 'flag', type: 'select', 
+          additionalProps: { 
+            mode: 'tags',
+            options: [  
+              { text: '热门', value: 'hot' },
+              { text: '推荐', value: 'recommend' },
+              { text: '置顶', value: 'top' },
+            ],
+            placeholder: '请输入标志' 
+          } as SelectProps, 
+        },
+        { 
+          label: '关键字', name: 'keywords', type: 'select', 
+          additionalProps: { 
+            mode: 'tags',
+            options: [],
+            placeholder: '请输入关键字,回车添加' 
+          } as SelectProps, 
+        },
+        { 
+          label: '描述', name: 'desc', type: 'text-area', 
+          additionalProps: { placeholder: '请输入描述' }, 
+        },
+        { 
+          label: 'TAG', name: 'tags', type: 'text', 
+          additionalProps: { placeholder: '请输入TAG' }, 
+        },
+        { 
+          label: '备注', name: 'memo', type: 'text-area', 
+          additionalProps: { placeholder: '请输入备注' }, 
+        },
+      ]
+    },
+  ],
+  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() {
+  formModel.value = await InheritorContent.getIchInfo();
+}
+</script>

+ 9 - 7
src/pages/inheritor.vue

@@ -12,7 +12,7 @@
        
         <a-tabs v-model:activeKey="activeKey" centered>
           <a-tab-pane key="1" tab="非遗项目">
-            <EmptyToRecord title="非遗项目" :model="ichData">
+            <EmptyToRecord title="非遗项目" :model="ichData" @edit="router.push({ name: 'FormIch' })">
               <a-descriptions class="mt-3" title="非遗项目信息" v-if="ichData" bordered :column="{ xs: 1, sm: 1, md: 1, lg: 2 }">
                 <a-descriptions-item label="标题"><ShowValueOrNull :value="ichData.title" /></a-descriptions-item>
                 <a-descriptions-item label="简介" :span="3"><SimpleRichHtml :contents="[ ichData.intro ]" /></a-descriptions-item>
@@ -91,6 +91,11 @@
                 
                 <a-descriptions-item label="单位类型"><ShowValueOrNull :value="seminarData.ichSiteTypeText" /></a-descriptions-item>
                 <a-descriptions-item label="单位"><ShowValueOrNull :value="seminarData.unit" /></a-descriptions-item>
+                
+                <a-descriptions-item v-if="seminarData.latitude && seminarData.longitude" label="地图">
+                  <SimplePointedMap :longitude="seminarData.longitude" :latitude="seminarData.latitude" :zoom="15" height="300px"  />
+                </a-descriptions-item>
+
                 <a-descriptions-item v-if="seminarData.image" label="图片">
                   <a-image :src="seminarData.image" style="max-width:300px;" />
                 </a-descriptions-item>
@@ -112,6 +117,8 @@
 </template>
 
 <script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
 import type { IchInfo, InheritorInfo, SeminarInfo } from '@/api/inheritor/InheritorContent';
 import InheritorContent from '@/api/inheritor/InheritorContent';
 import ImageGrid from '@/components/content/ImageGrid.vue';
@@ -119,17 +126,12 @@ import SimplePointedMap from '@/components/content/SimplePointedMap.vue';
 import SimpleRichHtml from '@/components/display/SimpleRichHtml.vue';
 import ShowValueOrNull from '@/components/dynamicf/Display/ShowValueOrNull.vue';
 import EmptyToRecord from '@/components/parts/EmptyToRecord.vue';
-import { useAuthStore } from '@/stores/auth';
-import { Modal } from 'ant-design-vue';
-import { onMounted, ref } from 'vue';
-import { useRouter } from 'vue-router';
 
 const activeKey = ref('1');
 const ichData = ref<IchInfo>();
 const inheritorData = ref<InheritorInfo>();
 const seminarData = ref<SeminarInfo>();
-
-
+const router = useRouter();
 
 onMounted(() => {
   InheritorContent.getIchInfo().then(data => {

+ 3 - 3
src/router/index.ts

@@ -11,9 +11,9 @@ const router = createRouter({
       component: Index,
     },
     {
-      path: '/form',
-      name: 'Form',
-      component: () => import('@/pages/form.vue'),
+      path: '/forms/ich',
+      name: 'FormIch',
+      component: () => import('@/pages/forms/ich.vue'),
     }, 
     {
       path: '/login',