快乐的梦鱼 vor 1 Woche
Ursprung
Commit
c6ff00cec5
40 geänderte Dateien mit 2589 neuen und 5 gelöschten Zeilen
  1. BIN
      ct_content_main_body_column.xlsx
  2. 384 2
      package-lock.json
  3. 6 1
      package.json
  4. 245 0
      src/api/BaseAppServerRequestModule.ts
  5. 419 0
      src/api/CommonContent.ts
  6. 12 0
      src/api/NotConfigue.ts
  7. 77 0
      src/api/RequestModules.ts
  8. 15 0
      src/api/Utils.ts
  9. 68 0
      src/api/auth/UserApi.ts
  10. 38 0
      src/api/index/IndexContent.ts
  11. 10 0
      src/api/index/IndexInrodContent.ts
  12. 10 0
      src/api/index/IndexNewsContent.ts
  13. 10 0
      src/api/news/NewsFolkActivitiesContent.ts
  14. 10 0
      src/api/news/NewsIndexContent.ts
  15. 10 0
      src/api/news/NewsOfficialActivitiesContent.ts
  16. 10 0
      src/api/news/NewsPeoplesStageContent.ts
  17. 44 0
      src/api/system/ConfigurationApi.ts
  18. 98 0
      src/common/ConvertRgeistry.ts
  19. 8 0
      src/common/EventBus.ts
  20. 20 0
      src/common/config/ApiCofig.ts
  21. 12 0
      src/common/config/AppCofig.ts
  22. 35 0
      src/common/utils/CommonUtils.ts
  23. 166 0
      src/components/Modal.vue
  24. 31 1
      src/components/PageHeader.vue
  25. 80 0
      src/components/SimplePageContentLoader.vue
  26. 137 0
      src/components/modalApi.ts
  27. 9 0
      src/composeable/LoaderCommon.ts
  28. 103 0
      src/composeable/PageAction.ts
  29. 8 0
      src/composeable/PagerDefine.ts
  30. 58 0
      src/composeable/SimpleDataLoader.ts
  31. 121 0
      src/composeable/SimplePagerData.ts
  32. 5 0
      src/main.ts
  33. 10 0
      src/router/index.ts
  34. 73 0
      src/stores/auth.ts
  35. 1 1
      src/views/culture-tourism/components/TourismModulePanel.vue
  36. 44 0
      src/views/generic-content/GenericContentView.vue
  37. 35 0
      src/views/generic-content/data.ts
  38. 56 0
      src/views/generic-list/GenericListView.vue
  39. 38 0
      src/views/generic-list/components/GenericListItem.vue
  40. 73 0
      src/views/generic-list/data.ts

BIN
ct_content_main_body_column.xlsx


+ 384 - 2
package-lock.json

@@ -10,9 +10,12 @@
       "dependencies": {
         "@iconify-json/solar": "^1.2.10",
         "@iconify/vue": "^5.0.1",
+        "@imengyu/imengyu-utils": "^0.1.3",
+        "@imengyu/js-request-transform": "^0.4.2",
         "@inquirer/prompts": "^8.7.2",
         "@tailwindcss/vite": "^4.3.3",
         "ali-oss": "^6.23.0",
+        "ant-design-vue": "^4.2.6",
         "archiver": "^8.0.0",
         "axios": "^1.20.0",
         "cli-progress": "^3.12.0",
@@ -24,6 +27,7 @@
         "json5": "^2.2.3",
         "md5": "^2.3.0",
         "minimatch": "^10.2.6",
+        "mitt": "^3.0.1",
         "pinia": "^4.0.2",
         "tailwindcss": "^4.3.3",
         "vue": "^3.5.40",
@@ -39,12 +43,41 @@
         "typescript": "~6.0.0",
         "vite": "^8.1.5",
         "vite-plugin-vue-devtools": "^8.1.5",
-        "vue-tsc": "^3.3.7"
+        "vue-tsc": "^3.3.7",
+        "xlsx": "^0.18.5"
       },
       "engines": {
         "node": "^22.18.0 || >=24.12.0"
       }
     },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.6.0.tgz",
+      "integrity": "sha512-PRomU725ABMf/lnQp5HiB7my1kjEbFY0D10N4lXYxK6TIB1gKjIVD5MRThpDaezLgw1D774J8eOeeDB0M6wHrQ==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.29.7",
       "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -453,6 +486,12 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-8.0.5.tgz",
+      "integrity": "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==",
+      "license": "MIT"
+    },
     "node_modules/@babel/template": {
       "version": "7.29.7",
       "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz",
@@ -510,6 +549,27 @@
         "node": ">=0.1.90"
       }
     },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
     "node_modules/@iconify-json/solar": {
       "version": "1.2.10",
       "resolved": "https://registry.npmmirror.com/@iconify-json/solar/-/solar-1.2.10.tgz",
@@ -540,6 +600,22 @@
         "vue": ">=3.0.0"
       }
     },
+    "node_modules/@imengyu/imengyu-utils": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.1.3.tgz",
+      "integrity": "sha512-I1hZ7yBEexQLDtPxHHy+ZSlTB2tQX27DkHfh2ztg6L6Gyyu78mFqWFZSvzUUDwplEiHvAMNGyFTWlLd6t/Hicw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^8.0.0",
+        "@imengyu/js-request-transform": "^0.4.0"
+      }
+    },
+    "node_modules/@imengyu/js-request-transform": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmmirror.com/@imengyu/js-request-transform/-/js-request-transform-0.4.2.tgz",
+      "integrity": "sha512-8tsTL9TDLVP77TYGjxqiUooMNFvBdM81qJa51l8yOxc09jvxm9Ma0Ws71RXX321HO8qQHv1+V8dC/FzlDR8rFw==",
+      "license": "MIT"
+    },
     "node_modules/@inquirer/ansi": {
       "version": "2.0.8",
       "resolved": "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-2.0.8.tgz",
@@ -1193,6 +1269,16 @@
       "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
       "license": "MIT"
     },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
     "node_modules/@tailwindcss/node": {
       "version": "4.3.3",
       "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.3.tgz",
@@ -2073,6 +2159,16 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
@@ -2172,6 +2268,55 @@
         "node": ">=14"
       }
     },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/ant-design-vue/node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/any-promise": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
@@ -2198,6 +2343,12 @@
         "node": ">=18"
       }
     },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
     "node_modules/ast-kit": {
       "version": "2.2.0",
       "resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
@@ -2237,6 +2388,12 @@
       "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
       "license": "MIT"
     },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
@@ -2557,6 +2714,20 @@
       ],
       "license": "CC-BY-4.0"
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chardet": {
       "version": "2.2.0",
       "resolved": "https://registry.npmmirror.com/chardet/-/chardet-2.2.0.tgz",
@@ -2623,6 +2794,16 @@
         "node": ">= 12"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/combined-stream": {
       "version": "1.0.8",
       "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2660,6 +2841,12 @@
         "node": ">=18"
       }
     },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
     "node_modules/confbox": {
       "version": "0.2.4",
       "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
@@ -2697,6 +2884,20 @@
       "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==",
       "license": "MIT"
     },
+    "node_modules/core-js": {
+      "version": "3.50.0",
+      "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.50.0.tgz",
+      "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -2790,6 +2991,12 @@
         "node": "*"
       }
     },
+    "node_modules/dayjs": {
+      "version": "1.11.23",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz",
+      "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
+      "license": "MIT"
+    },
     "node_modules/debug": {
       "version": "4.4.3",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
@@ -2899,6 +3106,18 @@
         "node": ">= 8.0.0"
       }
     },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3202,6 +3421,16 @@
         "pause-stream": "~0.0.11"
       }
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -3483,6 +3712,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-stream": {
       "version": "4.0.1",
       "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz",
@@ -3563,7 +3801,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/jsesc": {
@@ -3940,6 +4177,24 @@
       "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
       "license": "MIT"
     },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4069,6 +4324,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
     "node_modules/mkdirp": {
       "version": "0.5.6",
       "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -4170,6 +4431,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
     "node_modules/node-hex": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/node-hex/-/node-hex-1.0.1.tgz",
@@ -4629,6 +4896,12 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
     "node_modules/rolldown": {
       "version": "1.2.7",
       "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.7.tgz",
@@ -4710,6 +4983,15 @@
         "node": ">=11.0.0"
       }
     },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
     "node_modules/scule": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
@@ -4735,6 +5017,12 @@
         "semver": "bin/semver.js"
       }
     },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4879,6 +5167,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz",
@@ -4986,6 +5287,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/stylis": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz",
+      "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+      "license": "MIT"
+    },
     "node_modules/tailwindcss": {
       "version": "4.3.3",
       "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.3.tgz",
@@ -5056,6 +5363,15 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
     "node_modules/through": {
       "version": "2.3.8",
       "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
@@ -5599,6 +5915,30 @@
         "typescript": ">=5.0.0"
       }
     },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/webpack-virtual-modules": {
       "version": "0.6.2",
       "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@@ -5642,6 +5982,26 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
@@ -5678,6 +6038,28 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/xml2js": {
       "version": "0.6.2",
       "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz",

+ 6 - 1
package.json

@@ -14,9 +14,12 @@
   "dependencies": {
     "@iconify-json/solar": "^1.2.10",
     "@iconify/vue": "^5.0.1",
+    "@imengyu/imengyu-utils": "^0.1.3",
+    "@imengyu/js-request-transform": "^0.4.2",
     "@inquirer/prompts": "^8.7.2",
     "@tailwindcss/vite": "^4.3.3",
     "ali-oss": "^6.23.0",
+    "ant-design-vue": "^4.2.6",
     "archiver": "^8.0.0",
     "axios": "^1.20.0",
     "cli-progress": "^3.12.0",
@@ -28,6 +31,7 @@
     "json5": "^2.2.3",
     "md5": "^2.3.0",
     "minimatch": "^10.2.6",
+    "mitt": "^3.0.1",
     "pinia": "^4.0.2",
     "tailwindcss": "^4.3.3",
     "vue": "^3.5.40",
@@ -43,7 +47,8 @@
     "typescript": "~6.0.0",
     "vite": "^8.1.5",
     "vite-plugin-vue-devtools": "^8.1.5",
-    "vue-tsc": "^3.3.7"
+    "vue-tsc": "^3.3.7",
+    "xlsx": "^0.18.5"
   },
   "engines": {
     "node": "^22.18.0 || >=24.12.0"

+ 245 - 0
src/api/BaseAppServerRequestModule.ts

@@ -0,0 +1,245 @@
+import ApiCofig from "@/common/config/ApiCofig";
+import AppCofig, { isDev } from "@/common/config/AppCofig";
+import { useAuthStore } from "@/stores/auth";
+import { 
+  RequestCoreInstance, RequestApiError, StringUtils, appendGetUrlParams, 
+  appendPostParams, RequestResponse, RequestOptions, 
+  type RequestApiInfoStruct, RequestApiResult, 
+  type RequestApiErrorType, 
+  defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch, WebFetchImplementer } from "@imengyu/imengyu-utils";
+import { logError, type DataModel, type KeyValue, type NewDataModel } from "@imengyu/js-request-transform";
+import Modal from "@/components/Modal.vue";
+
+/**
+ * 说明:业务相关的请求模块
+ * * 请求数据处理函数。
+ * * 自定义请求模块。
+ * * 自定义错误报告处理函数。
+ */
+
+function matchNotReportMessage(code: number, str: string) {
+  if (ApiCofig.notReportErrorCode.includes(code))
+    return true;
+  for (let i = 0; i < ApiCofig.notReportMessages.length; i++) {
+    if (ApiCofig.notReportMessages[i]!.test(str))
+      return true;
+  }
+  return false;
+}
+//错误报告处理
+export function reportError<T extends DataModel>(instance: RequestCoreInstance<T>, response: RequestApiError | Error) {
+  if (isDev) {
+    //开发模式下直接弹窗显示
+    if (response instanceof RequestApiError) {
+      Modal.error({
+        title: `请求错误 ${response.apiName} : ${response.errorMessage}`,
+        content: response.toString() +
+          '\r\n请求接口:' + response.apiName +
+          '\r\n请求地址:' + response.apiUrl +
+          '\r\n请求参数:' + JSON.stringify(response.apiRawReq) +
+          '\r\n返回参数:' + JSON.stringify(response.rawData) +
+          '\r\n状态码:' + response.code +
+          '\r\n信息:' + response.errorCodeMessage,
+      });
+    } else {
+      Modal.error({
+        title: '错误报告 代码错误',
+        content: response?.stack || ('' + response),
+      });
+    }
+  } else {    
+    let errMsg = '';
+    if (response instanceof RequestApiError)
+      errMsg = response.errorMessage + '。';
+      
+    errMsg += '服务出现了异常,请稍后重试或联系客服。';
+    errMsg += '版本:' + AppCofig.version;
+
+    Modal.error({
+      title: '抱歉',
+      content: errMsg,
+      showCancel: false,
+    });
+  }
+}
+
+export class BaseAppServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor(baseUrl: string) {
+    super(WebFetchImplementer);
+    this.config.baseUrl = baseUrl;
+    this.config.errCodes = [];
+    //请求拦截器
+    this.config.requestInterceptor = (url, req) => {
+      //获取store中的token,追加到头;
+      const autoStore = useAuthStore();
+      if (StringUtils.isNullOrEmpty((req.headers as KeyValue).token as string)) {
+        if (!StringUtils.isNullOrEmpty(autoStore.token)) {
+          req.headers['token'] = autoStore.token;
+          req.headers['__token__'] = autoStore.token;
+        }
+      }
+      if (req.method == 'GET') {
+        //追加GET参数
+        url = appendGetUrlParams(url, 'main_body_id', ApiCofig.mainBodyId);
+        //url = appendGetUrlParams(url, 'platform', ApiCofig.platformId);
+      } else {
+        if (!(req.data instanceof FormData)) {
+          req.data = appendPostParams(req.data,'main_body_id', ApiCofig.mainBodyId);
+          //req.data = appendPostParams(req.data,'platform', ApiCofig.platformId);
+        } else {
+          req.data.append('main_body_id', ApiCofig.mainBodyId.toString());
+          //req.data = appendPostParams(req.data,'platform', ApiCofig.platformId);
+        }
+      } 
+  return { newUrl: url, newReq: req };
+    };
+    //响应数据处理函数
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>, apiInfo: RequestApiInfoStruct): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers,
+              apiInfo
+            );
+          }
+
+          //code == 0 错误
+          if (json.code === 0) {
+            throw createError(json);
+          }
+
+          //处理后端的数据
+          let message = '未知错误';
+          let data = {} as any;
+
+          //后端返回格式不统一,所以在这里处理格式
+          if (typeof json.data === 'object') {
+            data = json.data;
+            message = json.data?.msg || response.statusText;
+          }
+          else {
+            //否则返回上层对象
+            data = json;
+            message = json.msg || response.statusText;
+          }
+
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.code || response.status,
+            message,
+            data,
+            json,
+            response.headers,
+            apiInfo
+          );
+        }
+        else {
+          throw createError(json);
+        }
+
+        function createError(json: any): RequestApiError {
+          let errType : RequestApiErrorType = 'unknow';
+          let errString = '';
+          let errCodeStr = '';
+
+          if (typeof json.message === 'string') 
+            errString = json.message;
+          if (typeof json.msg === 'string') 
+            errString += json.msg;
+
+          if (StringUtils.isStringAllEnglish(errString))
+            errString = '服务器返回:' + errString;
+
+          //错误处理
+          if (errString) {
+            //如果后端有返回错误信息,则收集错误信息并返回
+            errType = 'businessError';
+            if (typeof json.data === 'object' && json.data?.errmsg) {
+              errString += '\n' + json.data.errmsg;
+            }
+            if (typeof json.errors === 'object') {
+              for (const key in json.errors) {
+                if (Object.prototype.hasOwnProperty.call(json.errors, key)) {
+                  errString += '\n' + json.errors[key];
+                }
+              }
+            }
+          } else {
+            const res = defaultResponseDataGetErrorInfo(response, json);
+            errType = res.errType;
+            errString = res.errString;
+            errCodeStr = res.errCodeStr;
+          }
+
+          const res = new RequestApiError(
+            errType,
+            errString,
+            errCodeStr,
+            response.status,
+            null,
+            null,
+            response.headers,
+            apiInfo
+          );
+          console.log(res);
+          return res;
+        }
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw err;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, apiInfo, response.url, reject, instance);
+        });
+      }
+    };    /**
+    * 此函数用于请求失败时,如何处理错误信息
+    * 
+    * NOTE: 这里配置Api请求时要如何处理错误
+    * @param err 错误信息实例 
+    * @param response 返回数据
+    * @returns 返回一个 Promise,可以定义最终调用该接口的代码会接收到什么样的错误对象。
+    */
+   this.config.responseErrorHandler = (err, instance, apiInfo) => {
+     function createErr(errorType: RequestApiErrorType, message: string) {
+       return new RequestApiError(errorType, message, undefined, undefined, undefined, err, undefined, apiInfo);
+     }
+     if (err instanceof Error) {
+       if (err.message == 'Network Error')
+         return createErr('networkError', '请求失败,请检查您的网络连接?');
+       if (err.message.includes('Timeout') 
+         || err.message.includes('timeout') 
+         || err.message.includes('singal'))
+         return createErr('networkError', '请求超时');
+       if (err.message.includes('cancelled') 
+         || err.message.includes('aborted'))
+         return createErr('networkError', '请求已取消');
+       if (err.message.includes('AbortError'))
+         return createErr('networkError', '请求失败,请检查您的网络连接?');
+       return createErr('scriptError', err.message);
+     } else if (err instanceof RequestApiError) {
+       return err;
+     }
+     return createErr('unknow', '请求失败,未知错误:' + err);
+    };
+    //错误报告处理
+    this.config.responseErrorReportInterceptor = (instance, response) => {
+      return (
+        (response.errorType !== 'businessError' && response.errorType !== 'networkError') ||
+        matchNotReportMessage(response.code, response.errorMessage) === true
+      );
+    };
+    //错误报告处理函数
+    this.config.reportError = reportError;
+  }
+}

+ 419 - 0
src/api/CommonContent.ts

@@ -0,0 +1,419 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from './RequestModules';
+import { transformSomeToArray } from './Utils';
+import type { QueryParams } from '@imengyu/imengyu-utils';
+
+export class GetColumListParams extends DataModel<GetColumListParams> {
+  
+  public constructor() {
+    super(GetColumListParams);
+    this.setNameMapperCase('Camel', 'Snake');
+  }
+
+  setModelId(val: number) {
+    this.modelId = val;
+    return this;
+  }
+  setMainBodyColumnId(val: number) {
+    this.mainBodyColumnId = val;
+    return this;
+  }
+  setFlag(val: 'hot'|'recommend'|'top') {
+    this.flag = val;
+    return this; 
+  }
+  setSize(val: number) {
+    this.size = val;
+    return this; 
+  }
+
+  modelId?: number;
+  /**
+   * 	主体栏目id
+   */
+  mainBodyColumnId: number = 0;
+  /**
+   * 标志:hot=热门,recommend=推荐,top=置顶
+   */
+  flag ?: 'hot'|'recommend'|'top';
+  /**
+   * 内容数量,默认4
+   */
+  size = 4;
+}
+export class GetContentListParams extends DataModel<GetContentListParams> {
+  
+  public constructor() {
+    super(GetContentListParams);
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      ids: {
+        customToServerFn: (val) => (val as number[]).join(','),
+        customToClientFn: (val) => (val as string).split(',').map((item) => parseInt(item)),
+      },
+    }
+  }
+
+
+  setMainBodyColumnId(val: number|number[]) {
+    this.mainBodyColumnId = val;
+    return this;
+  }
+  setFlag(val: 'hot'|'recommend'|'top') {
+    this.flag = val;
+    return this; 
+  }
+  setIds(val: number[]) {
+    this.ids = val;
+    return this; 
+  }
+  setType(val: 1|2|3|4) {
+    this.type = val;
+    return this;
+  }
+  setSize(val: number) {
+    this.size = val;
+    return this;
+  }
+  setKeywords(val: string) {
+    this.keywords = val;
+    return this; 
+  }
+  setModelId(val: number) {
+    this.modelId = val;
+    return this; 
+  }
+
+  static TYPE_ARTICLE = 1;
+  static TYPE_AUDIO = 2;
+  static TYPE_VIDEO = 3;
+  static TYPE_IMAGE = 4;
+
+  modelId ?: number;
+  /**
+   * 主体栏目id
+   */
+  mainBodyColumnId: number|number[] = 0;
+  /**
+   * 标志:hot=热门,recommend=推荐,top=置顶
+   */
+  flag ?: 'hot'|'recommend'|'top';
+  /**
+   * 内容id(逗号隔开)如:3 或者 1,2,3
+   */
+  ids?: number[];
+  /**
+   * 类型:1=文章,2=音频,3=视频,4=相册
+   */
+  type?: 1|2|3|4;
+  /**
+   * 内容数量,默认4
+   */
+  size = 4;
+  /**
+   * 关键字查询
+   */
+  keywords?: string;
+
+}
+
+export class GetColumContentList extends DataModel<GetColumContentList> {
+  constructor() {
+    super(GetColumContentList, "主体栏目列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      name: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
+      content_list: { 
+        clientSide: 'array',
+        clientSideRequired: true,
+        clientSideChildDataModel: GetContentListItem,
+      },
+    }
+  }
+
+  name = '';
+  overview = '';
+}
+export class GetContentListItem extends DataModel<GetContentListItem> {
+  constructor() {
+    super(GetContentListItem, "内容列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      mainBodyColumnId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      title: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
+      isGuest: { clientSide: 'boolean', serverSide: 'number' },
+      isLogin: { clientSide: 'boolean', serverSide: 'number' },
+      isComment: { clientSide: 'boolean', serverSide: 'number' },
+      isLike: { clientSide: 'boolean', serverSide: 'number' },
+      isCollect: { clientSide: 'boolean', serverSide: 'number' },
+      latitude: { clientSide: 'number', serverSide: 'number' },
+      longitude: { clientSide: 'number', serverSide: 'number' },
+      publish_at: { clientSide: 'date', serverSide: 'string' },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      type: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time') || key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+  }
+  id = 0;
+  mainBodyColumnId = 0;
+  latitude = 0;
+  longitude = 0;
+  mapX = '';
+  mapY = '';
+  from = '';
+  modelId = 0;
+  title = '!title';
+  region = 0;
+  image = '';
+  thumbnail = '';
+  desc = '!desc';
+  content = '!content';
+  type = 0;
+  keywords ?: string[];
+  flag ?: string[];
+  tags ?: string[];
+  views = 0;
+  comments = 0;
+  likes = 0;
+  collects = 0;
+  dislikes = 0;
+  district = '';
+  publishAt = new Date();
+}
+export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
+  constructor() {
+    super(GetContentDetailItem, "内容详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      title: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
+      content: { clientSide: 'string', serverSide: 'string' },
+      isGuest: { clientSide: 'boolean', serverSide: 'number' },
+      isLogin: { clientSide: 'boolean', serverSide: 'number' },
+      isComment: { clientSide: 'boolean', serverSide: 'number' },
+      isLike: { clientSide: 'boolean', serverSide: 'number' },
+      isCollect: { clientSide: 'boolean', serverSide: 'number' },
+      publish_at: { clientSide: 'date', serverSide: 'string' },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      type: { clientSide: 'number', serverSide: 'number' },
+      ichSitesList: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
+      inheritorsList: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
+      otherLevel: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time') || key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      else if (key.endsWith('List')) {
+        return [
+          { clientSide: 'map', serverSide: 'original'},
+          { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem, serverSide: 'original' },
+        ]
+      }
+      return undefined;
+    };
+    this._afterSolveServer = () => {
+      if (!this.image && this.images && this.images && this.images.length > 0  ) {
+        this.image = this.images[0]
+      }
+      if ((!this.images || this.images.length == 0) && this.image) {
+        this.images = [ this.image ]
+      }
+      if (this.publishVideo) {
+        this.video = this.publishVideo
+      }
+    }
+  }
+
+  id = 0;
+  from = '';
+  modelId = 0;
+  type = 0;
+  title = '';
+  region = 0;
+  image = '';
+  images = [] as string[];
+  audio = '';
+  video = '';
+  desc = '';
+  flag ?: string[];
+  tags ?: string[];
+  publishVideo?: string;
+  views = 0;
+  comments = 0;
+  likes = 0;
+  collects = 0;
+  dislikes = 0;
+  isLogin = false;
+  isGuest = false;
+  isComment = false;
+  isLike = false;
+  isCollect = false;
+  content = '';
+  publish_at = new Date();
+  associationMeList = [] as {
+    id: number,
+    title: string,
+    image: string,
+    thumbnail: string,
+  }[];
+  otherLevel : GetContentDetailItem[] = [];
+}
+
+export class CategoryListItem extends DataModel<CategoryListItem> {
+  constructor() {
+    super(CategoryListItem, "分类列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      pid: { clientSide: 'number', serverSide: 'number' },
+      haschild: { clientSide: 'boolean', serverSide: 'number' },
+    }
+  }
+
+  id !: number;
+  pid !: number;
+  title = '';
+  status = 'normal';
+  weight = 0;
+  spacer = '';
+  haschild = false;
+  children?: CategoryListItem[];
+}
+
+export class CommonContentApi extends AppServerRequestModule<DataModel> {
+
+  constructor(modelId: number, debugName: string, mainBodyColumnId?: number|number[]) {
+    super();
+    this.modelId = modelId;
+    this.mainBodyColumnId = mainBodyColumnId;
+    this.debugName = debugName;
+  }
+
+  public mainBodyColumnId?: number|number[];
+  public modelId: number;
+  protected debugName: string;
+
+  private toStringArray(arr: number|number[]|undefined) {
+    if (typeof arr === 'undefined') 
+      return '';
+    return typeof arr === 'object' ? arr.join(',') : arr.toString();
+  }
+
+
+  /**
+   * 获取分类列表
+   * @param type 根级类型:1=区域、2=级别、3=文物类型、4=非遗类型、42=事件类型
+   * @param withself 是否返回包含自己:true=是,false=否 ,默认false
+   * @returns 
+   */
+  async getCategoryList(
+    type?: number,
+    withself?: boolean,
+  ) {
+    return (this.get('/content/category/getCategoryList', '获取分类列表', {
+      type,
+      is_tree: false,
+      withself,
+    }))
+      .then(res => transformArrayDataModel<CategoryListItem>(CategoryListItem, res.data || [], `获取分类列表`, true))
+      .catch(e => { throw e });
+  }
+  /**
+   * 用于获取某一个分类需要用的子级
+   * @param pid 父级
+   * @returns 
+   */
+  async getCategoryChildList(pid?: number) {
+    return (this.get('/content/category/getCategoryOnlyChildList', '获取分类子级列表', {
+      pid,
+    }))
+      .then(res => transformArrayDataModel<CategoryListItem>(
+        CategoryListItem, 
+        transformSomeToArray(res.data), 
+        `获取分类列表`, 
+        true
+      ))
+      .catch(e => { throw e });
+  }
+  /**
+   * 主体栏目列表
+   * @param params 参数 
+   * @param querys 额外参数
+   * @returns 
+   */
+  getColumList<T extends DataModel = GetColumContentList>(params: GetColumListParams, modelClassCreator: NewDataModel = GetColumContentList, querys?: QueryParams) {
+    return this.get<{
+      list: T[],
+      total: number,
+    }>('/content/content/getMainBodyColumnContentList', `${this.debugName} 主体栏目列表`, {
+      model_id: this.modelId,
+      ...params.toServerSide(),
+      ...querys,
+    })
+      .then(res => ({
+        list: transformArrayDataModel<T>(modelClassCreator, res.data?.list || [], `${this.debugName} 主体栏目列表`, true),
+        total: res.data?.total as number || 0,
+      }))
+      .catch(e => { throw e });
+  }
+  /**
+   * 模型内容列表
+   * @param params 参数
+   * @param page 页码
+   * @param pageSize 页大小
+   * @param querys 额外参数
+   * @returns 
+   */
+  getContentList<T extends DataModel = GetContentListItem>(params: GetContentListParams, page: number, pageSize: number = 10, modelClassCreator: NewDataModel = GetContentListItem, querys?: QueryParams) {
+    return this.get<{
+      list: T[],
+      total: number,
+    }>('/content/content/getContentList', `${this.debugName} 模型内容列表`, {
+      ...params.toServerSide(),
+      ...querys,
+      model_id: params.modelId ?? this.modelId,
+      main_body_column_id: this.toStringArray(params.mainBodyColumnId || this.mainBodyColumnId),
+      page,
+      pageSize,
+    })
+      .then(res => ({ 
+        list: transformArrayDataModel<T>(modelClassCreator, res.data?.list || [], `${this.debugName} 模型内容列表`, true),
+        total: res.data?.total as number || 0,
+      }))
+      .catch(e => { throw e });
+  }
+  /**
+   * 内容详情
+   * @param id id 
+   * @param querys 额外参数
+   * @returns 
+   */
+  getContentDetail<T extends DataModel = GetContentDetailItem>(id: number, modelId?: number, modelClassCreator: NewDataModel = GetContentDetailItem, querys?: QueryParams) {
+    return this.get('/content/content/getContentDetail', `${this.debugName} (${id}) 内容详情`, {
+      model_id: modelId ?? this.modelId,
+      id,
+      ...querys,
+    }, modelClassCreator)
+      .then(res => res.data as T)
+      .catch(e => { throw e });
+  }
+}
+
+export const NO_CONTENT_STRING = '无内容!请添加内容!';
+
+export default new CommonContentApi(0, '通用内容');

+ 12 - 0
src/api/NotConfigue.ts

@@ -0,0 +1,12 @@
+import { DataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from './RequestModules';
+
+export class CommonApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+    this.config.modelClassCreator = DataModel;
+  }
+}
+
+export default new CommonApi();

+ 77 - 0
src/api/RequestModules.ts

@@ -0,0 +1,77 @@
+import { RequestResponse, RequestOptions, RequestCoreInstance, type RequestApiInfoStruct, RequestApiResult, RequestApiError, defaultResponseDataHandlerCatch } from "@imengyu/imengyu-utils";
+import type { DataModel, NewDataModel } from "@imengyu/js-request-transform";
+import { BaseAppServerRequestModule } from "./BaseAppServerRequestModule";
+import ApiCofig from "@/common/config/ApiCofig";
+import { isDev } from "@/common/config/AppCofig";
+
+/**
+ * 主应用服务请求模块
+ */
+export class AppServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
+  constructor() {
+    super(isDev ? ApiCofig.serverDev : ApiCofig.serverProd);
+  }
+}
+
+/**
+ * 更新服务请求模块
+ */
+export class UpdateServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
+  constructor() {
+    super("https://subserver.wenlvti.net/update");
+    this.config.requestInterceptor = undefined;
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>, apiInfo: RequestApiInfoStruct): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers,
+              apiInfo
+            );
+          }
+          if (!json.success)
+            throw new RequestApiError(
+              'businessError',
+              json.message,
+              json.code.toString(),
+              json.code,
+              json,
+              json,
+              response.headers,
+              apiInfo
+            );
+          
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.code || response.status,
+            json.message,
+            json.data,
+            json,
+            response.headers,
+            apiInfo
+          );
+        }
+        else {
+          throw json;
+        }
+
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw response;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, apiInfo, response.url, reject, instance);
+        });
+      }
+    };
+  }
+}

+ 15 - 0
src/api/Utils.ts

@@ -0,0 +1,15 @@
+export function transformSomeToArray(source: any) {
+  if (typeof source === 'string') 
+    return source.split(','); 
+  if (typeof source === 'object') {
+    if (source instanceof Array)
+      return source; 
+    else {
+      const arr = [];
+      for (const key in source)
+        arr.push(source[key]);
+      return arr;
+    }
+  }
+  return source;
+}

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

@@ -0,0 +1,68 @@
+import { DataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+
+
+export class LoginResult extends DataModel<LoginResult> {
+  constructor() {
+    super(LoginResult, "登录结果");
+    this._convertTable = {
+      userInfo: { clientSide: 'object', clientSideChildDataModel: UserInfo },
+    };
+    this._nameMapperServer = {
+      'userinfo': 'userInfo',
+      'mainBodyUserInfo': 'userInfo',
+    }
+    this._afterSolveServer = () => {
+      if (this.mainBodyUserInfo) {
+        this.userInfo.token = this.mainBodyUserInfo.token;
+      }
+    };
+  }
+  userInfo !:UserInfo;
+  mainBodyUserInfo?:UserInfo;
+}
+export class UserInfo extends DataModel<UserInfo> {
+  constructor() {
+    super(UserInfo, "用户信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+  }
+
+  expiresIn = 0;
+  id = 0;
+  userId = 0;
+  mobile = '';
+  nickname = '';
+  avatar = '';
+  username = '';
+  token = '';
+}
+
+export class UserApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+  async loginAdmin(data?: {
+    account: string,
+    password: string,
+  }) {
+    const form = new FormData();
+    form.append('account', data?.account || '');
+    form.append('password', data?.password || '');
+    return (await this.post('/user/adminLogin', '登录', form, undefined, LoginResult)).data as LoginResult;
+  }
+  async getUserInfo(main_body_user_id: number) {
+    return (await this.post('/content/main_body_user/getMainBodyUser', '获取用户信息', {
+      main_body_user_id,
+    }, undefined, UserInfo)).data as UserInfo;
+  }
+  async refresh() {
+    return (await this.post('/content/main_body_user/refreshUser', '刷新用户', {
+    }, undefined, LoginResult)).data as LoginResult;
+  }
+}
+
+export default new UserApi();

+ 38 - 0
src/api/index/IndexContent.ts

@@ -0,0 +1,38 @@
+import { DataModel } from '@imengyu/js-request-transform';
+import { CommonContentApi } from '../CommonContent';
+
+export class IndexStats extends DataModel<IndexStats> {
+  constructor() {
+    super(IndexStats, "内容详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      crData: { clientSide: 'forceArray' },
+      minnanCr: { clientSide: 'forceArray' },
+      historyData: { clientSide: 'forceArray' },
+      inheritorData: { clientSide: 'forceArray' },
+      ichData: { clientSide: 'forceArray' },
+      ichCenter: { clientSide: 'forceArray' },
+    }
+  }
+
+  crData: any;
+  minnanCr: any;
+  historyData: any;
+  inheritorData: any;
+  ichData: any;
+  ichCenter: any;
+}
+
+export class IndexContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "闽南文化-建筑文化", 232);
+  }
+
+  async getStats() {
+    return (await this.get('/volunteer/statistics/webData', '闽南文化首页数据统计', {
+    }, IndexStats)).data as IndexStats
+  }
+}
+
+export default new IndexContentApi();

+ 10 - 0
src/api/index/IndexInrodContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class IndexInrodContentApi extends CommonContentApi {
+
+  constructor() {
+    super(3, "闽南文化生态保护区概况", 234);
+  }
+}
+
+export default new IndexInrodContentApi();

+ 10 - 0
src/api/index/IndexNewsContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class IndexNewsContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "首页新闻", 232);
+  }
+}
+
+export default new IndexNewsContentApi();

+ 10 - 0
src/api/news/NewsFolkActivitiesContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class NewsFolkActivitiesContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "资讯动态-民俗庆典活动", 299);
+  }
+}
+
+export default new NewsFolkActivitiesContentApi();

+ 10 - 0
src/api/news/NewsIndexContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class NewsIndexContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "资讯动态");
+  }
+}
+
+export default new NewsIndexContentApi();

+ 10 - 0
src/api/news/NewsOfficialActivitiesContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class NewsOfficialActivitiesContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "资讯动态-官方活动", 228);
+  }
+}
+
+export default new NewsOfficialActivitiesContentApi();

+ 10 - 0
src/api/news/NewsPeoplesStageContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class NewsPeoplesStageContentApi extends CommonContentApi {
+
+  constructor() {
+    super(18, "资讯动态-百姓舞台", 298);
+  }
+}
+
+export default new NewsPeoplesStageContentApi();

+ 44 - 0
src/api/system/ConfigurationApi.ts

@@ -0,0 +1,44 @@
+import { UpdateServerRequestModule } from '@/api/RequestModules';
+import { DataModel } from '@imengyu/js-request-transform';
+
+export const CommonConfigurationConfig = {
+  /**
+   * 应用id
+   */
+  appId: 2,
+}
+
+export class ConfigurationApi extends UpdateServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * 获取当前配置,有缓存,会根据激活的历史版本获取对应配置
+   * @returns 
+   */
+  async getConfigById<T>(id: number) : Promise<T> {
+    return (await this.get<{
+      data: any
+    }>('/app-configuration-get', '获取配置', {
+      name: id,
+      appId: CommonConfigurationConfig.appId,
+    })).data!.data as T;
+  }
+  /**
+   * 更新配置
+   * @returns 
+   */
+  async updateConfigById<T>(id: number, config: T, accessKey: string) : Promise<void> {
+    (await this.post<{
+      data: any
+    }>('/app-configuration-edit/' + id, '获取配置', {
+      id: id,
+      accessKey: accessKey,
+      data: config,
+    })).data!.data as T;
+  }
+}
+
+export default new ConfigurationApi();

+ 98 - 0
src/common/ConvertRgeistry.ts

@@ -0,0 +1,98 @@
+import { 
+  DATA_MODEL_ERROR_REQUIRED_KEY_MISSING, 
+  DATA_MODEL_ERROR_TRY_CONVERT_BAD_TYPE, 
+  DATA_MODEL_ERROR_PRINT_SOURCE,
+  DATA_MODEL_ERROR_ARRAY_REQUIRED_KEY_MISSING,
+  DATA_MODEL_ERROR_ARRAY_IS_NOT_ARRAY,
+  DATA_MODEL_ERROR_MUST_PROVIDE_SIDE,
+  DATA_MODEL_ERROR_REQUIRED_KEY_NULL,
+  DataConverter, 
+  DataErrorFormatUtils, 
+  defaultDataErrorFormatHandler 
+} from "@imengyu/js-request-transform";
+
+function setErrorFormatter() {
+  DataErrorFormatUtils.setFormatHandler((error, data) => {
+    switch (error) {
+      case DATA_MODEL_ERROR_REQUIRED_KEY_MISSING: 
+        return `字段 ${data.sourceKey} 必填但未提供。 来源 ${data.source}; 对象 ${data.objectName} ${data.serverKey ? ('服务器应传字段: ' + data.serverKey) : ''}`;
+      case DATA_MODEL_ERROR_TRY_CONVERT_BAD_TYPE:
+        return `尝试将 ${data.sourceType} 转换为 ${data.targetType}。`;   
+      case DATA_MODEL_ERROR_PRINT_SOURCE:
+        return `来源: ${data.objectName}.`;
+      case DATA_MODEL_ERROR_ARRAY_REQUIRED_KEY_MISSING:
+        return `转换数组模型失败: 需要的字段 ${data.sourceKey} 未提供。`
+      case DATA_MODEL_ERROR_ARRAY_IS_NOT_ARRAY:
+        return `转换数组模型失败: 需要的字段 ${data.sourceKey} 不是数组类型。`
+      case DATA_MODEL_ERROR_MUST_PROVIDE_SIDE:
+        return `转换字段 ${data.key} 失败: 必须提供 ${data.direction} 侧数据。`;
+      case DATA_MODEL_ERROR_REQUIRED_KEY_NULL:
+        return `转换字段 ${data.key} 失败: 必填字段 ${data.key} 未提供或者为 null。`;
+    }
+    return defaultDataErrorFormatHandler(error, data);
+  });
+}
+
+export function registryConvert() {
+  setErrorFormatter();
+  DataConverter.registerConverter({
+    key: 'SplitCommaArray',
+    targetType: 'splitCommaArray',
+    converter: (source, key, type) => {
+      if (typeof source === 'string') 
+        return {
+          success: true,
+          result: source?.split(',') || [],
+        }
+      return {
+        success: false,
+        convertFailMessage: `[${key}] 不是字符串类型`,
+      };
+    }
+  })
+  DataConverter.registerConverter({
+    key: 'CommaArrayMerge',
+    targetType: 'commaArrayMerge',
+    converter: (source, key, type) => {
+      if (source instanceof Array) 
+        return {
+          success: true,
+          result: source?.join(',') || '',
+        }
+      return {
+        success: false,
+        convertFailMessage: `[${key}] 不是数组类型`,
+      };
+    }
+  })
+  DataConverter.registerConverter({
+    key: 'ForceArray',
+    targetType: 'forceArray',
+    converter: (source, key, type) => {
+      if (source instanceof Array) 
+        return {
+          success: true,
+          result: source,
+        }
+      if (typeof source === 'object' && source !== null) {
+        const arr = []
+        for (const key in source) {
+          arr.push((source as Record<string, any>)[key])
+        }
+        return {
+          success: true,
+          result: arr,
+        }
+      }
+      if (typeof source === 'string')
+        return {
+          success: true,
+          result: source.split(','), 
+        }
+      return {
+        success: false,
+        convertFailMessage: `[${key}] 不是数组类型`,
+      };
+    }
+  })
+}

+ 8 - 0
src/common/EventBus.ts

@@ -0,0 +1,8 @@
+import mitt from 'mitt'
+
+export type EventBusOnPageBackData = { name: string, data: any }
+type Events = {
+  pageActionListenOnPageBack: EventBusOnPageBackData,
+}
+
+export const EventBus = mitt<Events>();

+ 20 - 0
src/common/config/ApiCofig.ts

@@ -0,0 +1,20 @@
+
+/**
+ * 说明:后端接口配置
+ */
+export default {
+  serverDev: 'https://zhangzhou.wenlvti.net/api',
+  serverProd: 'https://zhangzhou.wenlvti.net/api',
+  mainBodyId: 1,
+  platformId: 327,
+  /**
+   * 不报告错误的 code
+   */
+  notReportErrorCode: [401],
+  /**
+   * 不报告错误的 message
+   */
+  notReportMessages: [
+    /请授权绑定手机号/g,
+  ] as RegExp[],
+}

+ 12 - 0
src/common/config/AppCofig.ts

@@ -0,0 +1,12 @@
+
+/**
+ * 说明:应用静态配置
+ */
+export default {
+  version: '0.0.1',
+}
+
+/**
+ * 是否是开发环境
+ */
+export const isDev = import.meta.env.DEV;

+ 35 - 0
src/common/utils/CommonUtils.ts

@@ -0,0 +1,35 @@
+export function stringNumberOrUndefined(val?: any) : undefined|number {
+  if (val) {
+    const v = Number(val);
+    return isNaN(v)? undefined : v;
+  }
+  return undefined;
+}
+export function printSpecificElement(element: string, style?: string) {
+    const targetElement = document.querySelector(element); // 获取要打印的元素
+    if (!targetElement) {
+        console.error('目标元素不存在');
+        return;
+    }
+    const styles = document.createElement('style');
+    styles.textContent = style || '';
+    const newWindow = window.open('', '_blank');
+    if (!newWindow) {
+        console.error('无法打开新窗口');
+        return;
+    }
+    newWindow.document.write(`
+        <html>
+            <head>
+                <script>
+                    window.onload = function() {
+                        window.print();
+                    };
+                </script>
+                ${styles.outerHTML}
+            </head>
+          <body>${targetElement.outerHTML}</body>
+        </html>
+    `);
+    newWindow.document.close();
+}

+ 166 - 0
src/components/Modal.vue

@@ -0,0 +1,166 @@
+<template>
+  <Teleport to="body">
+    <Transition name="modal-fade">
+      <div v-if="show" class="modal-overlay" @mousedown.self="onMaskClick">
+        <div class="modal-box" :style="boxStyle">
+          <div class="modal-header" :class="{'hide-title': title === ''}">
+            <div class="modal-title">
+              <slot name="title">{{ title }}</slot>
+            </div>
+          </div>
+          <div class="modal-body">
+            <slot />
+          </div>
+          <div v-if="$slots.footer" class="modal-footer">
+            <slot name="footer" />
+          </div>
+        </div>
+      </div>
+    </Transition>
+  </Teleport>
+</template>
+
+<script lang="ts">
+import { ModalApi } from './modalApi';
+
+export default {
+  name: 'Modal',
+  ...ModalApi,
+};
+</script>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+const props = withDefaults(defineProps<{
+  title?: string;
+  show: boolean;
+  showClose?: boolean;
+  width?: string | number;
+  height?: string | number;
+  maskClosable?: boolean;
+}>(), {
+  title: '',
+  showClose: true,
+  width: 480,
+  height: 'auto',
+  maskClosable: true,
+});
+
+const emit = defineEmits<{
+  (e: 'update:show', value: boolean): void;
+  (e: 'close'): void;
+}>();
+
+const boxStyle = computed(() => ({
+  width: toCssSize(props.width),
+  height: toCssSize(props.height),
+}));
+
+function toCssSize(value: string | number) {
+  return typeof value === 'number' ? `${value}px` : value;
+}
+
+function onClose() {
+  emit('update:show', false);
+  emit('close');
+}
+
+function onMaskClick() {
+  if (props.maskClosable) {
+    onClose();
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.modal-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0, 0, 0, 0.2);
+  backdrop-filter: blur(4px);
+}
+
+.modal-box {
+  display: flex;
+  flex-direction: column;
+  max-width: calc(100vw - 40px);
+  max-height: calc(100vh - 40px);
+  overflow: hidden;
+  border-radius: var(--border-radius);
+  border: 1px solid var(--color-border-light);
+  background: color-mix(in srgb, var(--color-surface) 80%, transparent);
+  backdrop-filter: blur(24px);
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.16);
+  color: var(--color-text);
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 12px 16px;
+  border-bottom: 1px solid var(--color-border-light);
+  flex-shrink: 0;
+
+  &.hide-title {
+    position: absolute;
+    top: 0;
+    right: 0;
+    z-index: 1000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-bottom: none
+  }
+}
+
+.modal-title {
+  flex: 1;
+  min-width: 0;
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--color-text);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.modal-body {
+  flex: 1;
+  min-height: 0;
+  padding: 16px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+}
+
+.modal-footer {
+  flex-shrink: 0;
+  padding: 12px 16px;
+  border-top: 1px solid var(--color-border-light);
+}
+
+.modal-fade-enter-active,
+.modal-fade-leave-active {
+  transition: opacity 0.2s ease-in-out;
+
+  .modal-box {
+    transition: transform 0.2s ease-in-out;
+  }
+}
+
+.modal-fade-enter-from,
+.modal-fade-leave-to {
+  opacity: 0;
+
+  .modal-box {
+    transform: scale(0.96);
+  }
+}
+</style>

+ 31 - 1
src/components/PageHeader.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { RouterLink, useRoute } from 'vue-router'
+import { RouterLink, useRoute, useRouter } from 'vue-router'
 
 withDefaults(
   defineProps<{
@@ -23,6 +23,11 @@ const navItems = [
 ]
 
 const route = useRoute()
+const router = useRouter()
+
+function goBack() {
+  router.back()
+}
 </script>
 
 <template>
@@ -40,6 +45,31 @@ const route = useRoute()
       "
     ></div>
 
+    <button
+      v-if="route.path !== '/'"
+      type="button"
+      aria-label="返回上一页"
+      title="返回上一页"
+      class="grid h-9 w-9 shrink-0 place-items-center rounded border border-[#d3ae65]/60 text-[#f2c66f] transition hover:border-[#d3ae65] hover:bg-[#b64d3c]"
+      @click="goBack"
+    >
+      <svg
+        aria-hidden="true"
+        class="h-5 w-5"
+        fill="none"
+        viewBox="0 0 24 24"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M15 5L8 12L15 19"
+          stroke="currentColor"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+          stroke-width="2"
+        ></path>
+      </svg>
+    </button>
+
     <div class="flex min-w-0 flex-1 items-center gap-5">
       <div class="min-w-0">
         <div

+ 80 - 0
src/components/SimplePageContentLoader.vue

@@ -0,0 +1,80 @@
+<template>
+  <div
+    v-if="loader?.loadStatus.value == 'loading'"
+    style="min-height: 200rpx;display: flex;justify-content: center;align-items: center;"
+  >
+    <a-spin tip="加载中" />
+  </div>
+  <div
+    v-else-if="loader?.loadStatus.value == 'error'"
+    style="min-height: 200rpx"
+  >
+    <a-empty :description="loader.loadError.value" >
+      <a-button  @click="handleRetry">重试</a-button>
+    </a-empty>
+  </div>
+  <template v-else-if="loader?.loadStatus.value == 'finished' || loader?.loadStatus.value == 'nomore'">
+    <slot />
+  </template>
+  <div
+    v-if="showEmpty || loader?.loadStatus.value == 'nomore'"
+    style="min-height: 200rpx"
+    class="empty"
+  >
+    <a-empty :description="emptyView?.text ?? '暂无数据'">
+      <a-button
+        v-if="emptyView?.button"
+        @click="emptyView?.buttonClick ?? handleRetry"
+      >
+        {{emptyView?.buttonText ?? '刷新'}}
+      </a-button>
+    </a-empty>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { ILoaderCommon } from '@/composeable/LoaderCommon';
+import { onMounted, ref, type PropType } from 'vue';
+
+const props = defineProps({	
+  loader: {
+    type: Object as PropType<ILoaderCommon<any>>,
+    default: null,
+  },
+  autoLoad: {
+    type: Boolean,
+    default: false, 
+  },
+  showEmpty: {
+    type: Boolean,
+    default: false, 
+  },
+  emptyView: {
+    type: Object as PropType<{
+      text: string,
+      buttonText: string,
+      button: boolean,
+      buttonClick: () => void,
+    }>,
+    default: null,
+  },
+})
+
+const loaded = ref(false);
+
+onMounted(() => {
+  loaded.value = false;
+  if (props.autoLoad)
+    handleLoad(); 
+});
+
+function handleRetry() {
+  props.loader.loadData(undefined);
+}
+function handleLoad() {
+  if (loaded.value) 
+    return;
+  loaded.value = true;
+  props.loader.loadData(undefined);
+}
+</script>

+ 137 - 0
src/components/modalApi.ts

@@ -0,0 +1,137 @@
+import { defineComponent, h, onMounted, render, ref, type VNode } from 'vue';
+import Modal from './Modal.vue';
+import Button from './Button.vue';
+import Icon from '@/components/Icon.vue';
+
+export type ModalConfirmType = 'info' | 'success' | 'warning' | 'error' | 'confirm';
+
+export interface ModalConfirmOptions {
+  title?: string;
+  content?: string | VNode;
+  okText?: string;
+  cancelText?: string;
+  width?: string | number;
+  maskClosable?: boolean;
+  showCancel?: boolean;
+  onOk?: () => void | Promise<void>;
+  onCancel?: () => void;
+}
+
+const typeConfig: Record<ModalConfirmType, { icon: string; color: string }> = {
+  info: { icon: 'icon-info-fill', color: '#1677ff' },
+  success: { icon: 'icon-round-check-fill', color: '#00c49f' },
+  warning: { icon: 'icon-warn-fill', color: '#ff8b2c' },
+  error: { icon: 'icon-round-close-fill', color: '#ff4d4f' },
+  confirm: { icon: 'icon-question-fill', color: '#ff8b2c' },
+};
+
+const ModalHolder = defineComponent({
+  name: 'ModalConfirmHolder',
+  props: {
+    type: { type: String, required: true },
+    options: { type: Object, required: true },
+    onDestroy: { type: Function, required: true },
+  },
+  setup(props) {
+    const show = ref(false);
+    const okLoading = ref(false);
+    const opt = props.options as ModalConfirmOptions;
+    const cfg = typeConfig[props.type as ModalConfirmType];
+
+    onMounted(() => {
+      show.value = true;
+    });
+
+    function destroy() {
+      (props.onDestroy as () => void)();
+    }
+
+    function handleClose() {
+      show.value = false;
+    }
+
+    function handleUpdateShow(value: boolean) {
+      show.value = value;
+      if (!value) {
+        setTimeout(destroy, 200);
+      }
+    }
+
+    function handleOk() {
+      const result = opt.onOk?.();
+      if (result && typeof (result as Promise<void>).then === 'function') {
+        okLoading.value = true;
+        Promise.resolve(result)
+          .then(handleClose)
+          .catch(() => {})
+          .finally(() => {
+            okLoading.value = false;
+          });
+      } else {
+        handleClose();
+      }
+    }
+
+    function handleCancel() {
+      opt.onCancel?.();
+      handleClose();
+    }
+
+    return () =>
+      h(
+        Modal,
+        {
+          show: show.value,
+          'onUpdate:show': handleUpdateShow,
+          title: opt.title,
+          width: opt.width,
+          maskClosable: opt.maskClosable,
+        },
+        {
+          default: () =>
+            h('div', { class: 'flex items-start gap-3' }, [
+              h(Icon, { name: cfg.icon, size: '1.5rem', color: cfg.color, class: 'flex-shrink-0 mt-0.5' }),
+              opt.content
+                ? h(
+                    'div',
+                    { class: 'flex-1 text-sm whitespace-pre-wrap break-words' },
+                    typeof opt.content === 'string' ? opt.content : opt.content,
+                  )
+                : null,
+            ]),
+          footer: () =>
+            h('div', { class: 'flex flex-row justify-end items-center gap-2' }, [
+              opt.showCancel
+                ? h(Button, { onClick: handleCancel }, { default: () => opt.cancelText ?? '取消' })
+                : null,
+              h(
+                Button,
+                { type: 'primary', loading: okLoading.value, onClick: handleOk },
+                { default: () => opt.okText ?? '确定' },
+              ),
+            ]),
+        },
+      );
+  },
+});
+
+function openConfirm(type: ModalConfirmType, options: ModalConfirmOptions = {}) {
+  const container = document.createElement('div');
+  document.body.appendChild(container);
+
+  const destroy = () => {
+    render(null, container);
+    container.remove();
+  };
+
+  const vm = h(ModalHolder, { type, options, onDestroy: destroy });
+  render(vm, container);
+}
+
+export const ModalApi = {
+  info: (options?: ModalConfirmOptions) => openConfirm('info', { showCancel: false, ...options }),
+  success: (options?: ModalConfirmOptions) => openConfirm('success', { showCancel: false, ...options }),
+  warning: (options?: ModalConfirmOptions) => openConfirm('warning', { showCancel: false, ...options }),
+  error: (options?: ModalConfirmOptions) => openConfirm('error', { showCancel: false, ...options }),
+  confirm: (options?: ModalConfirmOptions) => openConfirm('confirm', { showCancel: true, ...options }),
+};

+ 9 - 0
src/composeable/LoaderCommon.ts

@@ -0,0 +1,9 @@
+import type { Ref } from "vue";
+
+export type LoaderLoadType = 'loading' | 'finished' | 'nomore' | 'error';
+
+export interface ILoaderCommon<P> {
+  loadError: Ref<string>;
+  loadStatus: Ref<LoaderLoadType>;
+  loadData: (params?: P, refresh?: boolean) => Promise<void>;
+}

+ 103 - 0
src/composeable/PageAction.ts

@@ -0,0 +1,103 @@
+import { EventBus } from "@/common/EventBus";
+import { onBeforeUnmount, onMounted, type App } from "vue";
+import { useRoute, useRouter, type LocationQueryRaw } from "vue-router";
+
+/**
+ * 说明:页面导航相关函数封装。
+ */
+
+type OnPageBackCb = (data: Record<string, unknown>) => void
+
+const EventBusName = 'pageActionListenOnPageBack';
+
+export function useOnPageBack() {
+
+  const route = useRoute();
+  const cbs : OnPageBackCb[]  = [] 
+
+  function onPageBack(cb: OnPageBackCb) {
+    cbs.push(cb)
+  }
+
+  onMounted(() => {
+    EventBus.on(EventBusName, (e) => {
+      if (e.name === route.name) {
+        cbs.forEach((cb) => {
+          cb(e.data);
+        })
+      }
+    });
+  });
+  onBeforeUnmount(() => {
+    EventBus.off(EventBusName);
+  });
+
+  return {
+    onPageBack,
+  }
+}
+
+export function usePageAction() {
+  const router = useRouter();
+
+  /**
+   * 页面跳转: 后退至上一个页面。
+   */
+  function back() {
+    router.back();
+  }
+  /**
+   * 页面跳转: 后退并返回数据至上一个页面的 onPageBack 方法。
+   * @param data 要返回的数据
+   */
+  function backReturnData(data: Record<string, unknown>) {
+    callPrevOnPageBack('' + router.options.history.state.back, data);
+    router.back();
+  }
+  /**
+   * 页面跳转: 跳转到指定页面
+   * @param url 页面路径
+   * @param data 要传递的数据
+   */
+  function navTo(url: string, data: Record<string, unknown> = {}) {
+    const data2 : LocationQueryRaw = {}
+
+    for (const key in data) {
+      if (Object.prototype.hasOwnProperty.call(data, key))
+        data2[key] = data[key] as string;
+    }
+
+    router.push({
+      path: url,
+      query: data2,
+    });
+  }
+  /**
+   * 页面数据传递: 调用上一个页面的 onPageBack 方法
+   * @param name 方法名
+   * @param data 要传递的数据
+   */
+  function callPrevOnPageBack(name: string, data: Record<string, unknown>) {
+    EventBus.emit(EventBusName, {
+      name,
+      data,
+    })
+  }
+  /**
+   * 页面跳转: 调用上一个页面的 onPageBack 方法并返回至上一个页面
+   * @param name 方法名
+   * @param data 要传递的数据
+   */
+  function backAndCallOnPageBack(name: string, data: Record<string, unknown>) {
+    router.back();
+    callPrevOnPageBack(name, data);
+  }
+ 
+  return {
+    back,
+    backReturnData,
+    backAndCallOnPageBack, 
+    navTo,
+    callPrevOnPageBack,
+  }
+}

+ 8 - 0
src/composeable/PagerDefine.ts

@@ -0,0 +1,8 @@
+import type { Ref } from "vue";
+
+export interface PagerLoaderStateControl {
+  load: () => Promise<void>;
+  loading: Ref<boolean>;
+  empty: Ref<boolean>;
+  error: Ref<string>;
+}

+ 58 - 0
src/composeable/SimpleDataLoader.ts

@@ -0,0 +1,58 @@
+import { onMounted, ref, type Ref } from "vue";
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+import { formatError } from "@imengyu/imengyu-utils";
+
+export interface ISimpleDataLoader<T, P> extends ILoaderCommon<P> {
+  content: Ref<T|null>;
+  getLastParams: () => P | undefined;
+}
+
+export function useSimpleDataLoader<T, P = any>(
+  loader: (params?: P) => Promise<T>,
+  loadWhenMounted = true,
+  emptyIfArrayEmpty = true,
+)  : ISimpleDataLoader<T, P>
+ {
+
+  const content = ref<T|null>(null) as Ref<T|null>;
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+
+  let lastParams: P | undefined;
+
+  async function loadData(params?: P) {
+    if (params)
+      lastParams = params;
+    loadStatus.value = 'loading';
+    try {
+      const res = (await loader(params ?? lastParams)) as T;
+      content.value = res;
+      if (Array.isArray(res) && emptyIfArrayEmpty && (res as any[]).length === 0)
+        loadStatus.value = 'nomore';
+      else
+        loadStatus.value = 'finished';
+      loadError.value = '';
+    } catch(e) {
+      loadError.value = formatError(e);
+      loadStatus.value = 'error';
+      console.log(e);
+      
+    }
+  }
+
+  onMounted(() => {
+    if (loadWhenMounted) {
+      setTimeout(() => {
+        loadData();
+      }, (0.5 + Math.random()) * 500);
+    }
+  })
+
+  return {
+    content,
+    loadStatus,
+    loadError,
+    loadData,
+    getLastParams: () => lastParams,
+  }
+}

+ 121 - 0
src/composeable/SimplePagerData.ts

@@ -0,0 +1,121 @@
+import { formatError } from "@imengyu/imengyu-utils";
+import { watch, ref, computed, type Ref } from "vue"
+
+/**
+ * 简单分页数据封装。
+ * 
+ * 该封装了分页数据的加载、分页、上一页、下一页等功能。当页码发生变化时,会自动调用加载函数。
+ * 简单分页同时只能显示一页数据,重新加载会覆盖之前的数据。
+ * 
+ * 使用示例:
+ * ```ts
+ * const { data, page, total, loading } = useSimplePagerData(10, async (page, pageSize) => {
+ *   const res = await fetch(`/api/data?page=${page}&pageSize=${pageSize}`);
+ *   const data = await res.json();  
+ *   return {
+ *     data,
+ *     page: res.page,
+ *     total: res.total,
+ *   };
+ * });
+ * ```
+ *
+ * @param pageSize 一页的数量
+ * @param loader 加载函数
+ * @returns 
+ */
+export function useSimplePagerData<T>(
+  pageSize: number|Ref<number>, 
+  loader: (page: number, pageSize: number) => Promise<{ 
+    data: T[], 
+    page: number,
+    total: number,
+  }>
+) {
+  const data = ref<T[]>([]);
+  const page = ref(0);
+  const total = ref(0);
+  const totalPages = computed(() => Math.ceil(total.value / (typeof pageSize === 'number' ? pageSize : pageSize.value)));
+  const loading = ref(false);
+  const empty = computed(() => data.value.length === 0);
+  const error = ref('');
+
+  watch(page, async () => {
+    await load();
+  });
+
+  /**
+   * 加载数据
+   * @returns 
+   */
+  async function load() {
+    if (loading.value) 
+      return;
+    loading.value = true;
+    try {
+      error.value = '';
+      const res = await loader(page.value, typeof pageSize === 'number' ? pageSize : pageSize.value);
+      page.value = res.page;
+      total.value = res.total;
+      data.value = res.data;
+    } catch (e) {
+      console.error(e);
+      error.value = formatError(e);
+    } finally {
+      loading.value = false;
+    }
+  }
+  /**
+   * 下一页
+   */
+  async function next() {
+    if (page.value > total.value)
+      return;
+    page.value++;
+    await load();
+  }
+  /**
+   * 上一页
+   */
+  async function prev() {
+    if (page.value <= 1)
+      return;   
+    page.value--;
+    await load();
+  }
+
+  return {
+    load,
+    next,
+    prev,
+    /**
+     * 数据
+     */
+    data,
+    /**
+     * 是否为空
+     */
+    empty,
+    /**
+     * 加载错误信息
+     * 当加载失败时,该值不为空。
+     */
+    error,
+    /**
+     * 当前页码
+     */
+    page,
+    /**
+     * 总数据条数
+     */
+    total,
+    /**
+     * 总页数
+     */
+    totalPages,
+    /**
+     * 是否正在加载
+     */
+    loading,
+  }
+}

+ 5 - 0
src/main.ts

@@ -4,19 +4,24 @@ import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { addCollection, Icon } from '@iconify/vue'
 import solarIcons from './assets/solar-icons.json'
+import AntDesignVue from 'ant-design-vue'
+import 'ant-design-vue/dist/reset.css'
 
 import App from './App.vue'
 import router from './router'
 import { VChart } from './plugins/echarts'
 import PanelCard from './components/PanelCard.vue'
 import ProgressBar from './components/ProgressBar.vue'
+import { registryConvert } from './common/ConvertRgeistry.ts'
 
 addCollection(solarIcons)
+registryConvert();
 
 const app = createApp(App)
 
 app.use(createPinia())
 app.use(router)
+app.use(AntDesignVue)
 app.component('VChart', VChart)
 app.component('Icon', Icon)
 app.component('PanelCard', PanelCard)

+ 10 - 0
src/router/index.ts

@@ -39,6 +39,16 @@ const router = createRouter({
       name: 'digital-archives',
       component: () => import('../views/digital-archives/DigitalArchivesView.vue'),
     },
+    {
+      path: '/generic-list',
+      name: 'generic-list',
+      component: () => import('../views/generic-list/GenericListView.vue'),
+    },
+    {
+      path: '/generic-content',
+      name: 'generic-content',
+      component: () => import('../views/generic-content/GenericContentView.vue'),
+    },
   ],
 })
 

+ 73 - 0
src/stores/auth.ts

@@ -0,0 +1,73 @@
+import UserApi, { LoginResult, UserInfo } from "@/api/auth/UserApi";
+import { defineStore } from "pinia"
+
+const STORAGE_KEY = 'authInfo';
+
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    token: '',
+    expireAt: 0,
+    userMainBodyId: 0,
+    userId: 0,
+    userInfo: null as null|UserInfo,
+  }),
+  actions: {
+    async loadLoginState() {
+      try {
+        const res = localStorage.getItem(STORAGE_KEY);
+        if (!res)
+          throw 'no storage';
+        const authInfo = JSON.parse(res);
+        this.token = authInfo.token;
+        this.userId = authInfo.userId;
+        this.userInfo = authInfo.userInfo;
+        this.expireAt = authInfo.expireAt;
+        this.userMainBodyId = authInfo.userMainBodyId;
+      } catch (error) {
+        this.token = '';
+        this.userId = 0;
+        this.userInfo = null;
+        this.userMainBodyId = 0;
+
+        console.log('loadLoginState', error);
+      }
+    },
+    async loginAdmin(account: string, password: string) {
+      const loginResult = await UserApi.loginAdmin({
+        account,
+        password,
+      })
+      this.loginResultHandle(loginResult);
+    },
+    async loginResultHandle(loginResult: LoginResult) {
+      this.token = loginResult.userInfo.token;
+      this.userId = loginResult.userInfo.id;
+      this.userInfo = loginResult.userInfo;
+      this.userMainBodyId = loginResult.userInfo.mainBodyId as number;
+      this.expireAt = loginResult.userInfo.expiresIn + Date.now();
+
+      localStorage.setItem(STORAGE_KEY, 
+        JSON.stringify({ 
+          token: this.token, 
+          userId: this.userId ,
+          expireAt: this.expireAt,
+          userInfo: this.userInfo,
+          userMainBodyId: this.userMainBodyId,
+        }) 
+      );
+    },
+    async logout() {
+      this.token = '';
+      this.userId = 0;
+      this.userInfo = null;
+      this.userMainBodyId = 0;
+
+      localStorage.removeItem(STORAGE_KEY);
+    }
+  },
+  getters: {
+    isLogged(state) {
+      return state.token != '' && state.userId != 0
+    },
+  },
+})

+ 1 - 1
src/views/culture-tourism/components/TourismModulePanel.vue

@@ -23,7 +23,7 @@ defineProps<{ module: TourismModule }>()
         class="group border border-[#704033] bg-[#2b1514]/80 p-3 transition duration-300 hover:-translate-y-1 hover:border-[#d1734e] hover:bg-[#3a1c18]"
       >
         <div
-          class="relative aspect-[16/9] scale-x-[0.9] overflow-hidden border border-[#8e4c36] bg-gradient-to-br from-[#7c3f30] via-[#4b211e] to-[#b27546]"
+          class="relative w-full max-h-80 scale-x-[0.9] overflow-hidden border border-[#8e4c36] bg-gradient-to-br from-[#7c3f30] via-[#4b211e] to-[#b27546]"
         >
           <img
             v-if="card.image"

+ 44 - 0
src/views/generic-content/GenericContentView.vue

@@ -0,0 +1,44 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+import DashboardLayout from '@/components/DashboardLayout.vue'
+
+import { mockGenericContent } from './data'
+
+const route = useRoute()
+
+// 输入参数:modelId / id
+const modelId = computed(() => route.query.modelId as string | undefined)
+const id = computed(() => route.query.id as string | undefined)
+
+// 后续替换为接口调用:根据 modelId / id 拉取详情
+const content = mockGenericContent
+</script>
+
+<template>
+  <DashboardLayout subtitle="通用内容" footer-middle="通用内容页 · 数据驾驶舱">
+    <article class="mx-auto mt-5 max-w-4xl">
+      <section class="border-b border-[#7f4034]/70 pb-5">
+        <h1 class="text-[30px] font-bold leading-snug tracking-[.16em] text-[#fff0bd]">
+          {{ content.title }}
+        </h1>
+        <p class="mt-3 text-sm text-[#d8b45e]">{{ content.subtitle }}</p>
+        <div class="mt-4 flex items-center gap-4 text-xs text-[#c6a18c]">
+          <span class="flex items-center gap-1">
+            <Icon icon="solar:user-id-linear" width="14" />
+            {{ content.author }}
+          </span>
+          <span class="flex items-center gap-1">
+            <Icon icon="solar:calendar-mark-linear" width="14" />
+            {{ content.date }}
+          </span>
+        </div>
+      </section>
+
+      <section
+        class="generic-content mt-5 space-y-4 text-sm leading-7 text-[#e4c9ad]"
+        v-html="content.content"
+      ></section>
+    </article>
+  </DashboardLayout>
+</template>

+ 35 - 0
src/views/generic-content/data.ts

@@ -0,0 +1,35 @@
+/**
+ * 通用内容页静态数据
+ *
+ * 说明:以下均为静态示例数据,用于前端页面搭建。
+ * 后续对接后端时,请根据入参 modelId / id 调用详情接口,
+ * 将 `mockGenericContent` 替换为接口返回数据,
+ * 类型定义(interface)可保持不变,作为接口数据结构的约定。
+ */
+
+export interface GenericContent {
+  id: string
+  /** 标题 */
+  title: string
+  /** 副标题 */
+  subtitle: string
+  /** 作者 */
+  author: string
+  /** 日期 */
+  date: string
+  /** 文章内容(富文本 HTML) */
+  content: string
+}
+
+export const mockGenericContent: GenericContent = {
+  id: '1',
+  title: '歌仔戏传承人在漳州古城开课',
+  subtitle: '传统戏剧 · 国家级非遗代表性项目',
+  author: '漳州日报',
+  date: '2026-08-18',
+  content: `
+    <p>歌仔戏是闽南地区最具代表性的地方戏曲之一,以其婉转唱腔与生活化表演深受群众喜爱。近年来,漳州市持续推进歌仔戏的保护与传承工作,推动代表性传承人走进古城、走进社区,开展常态化传习授课。</p>
+    <p>本次在漳州古城开设的传习课堂,邀请国家级代表性传承人现场教学,围绕经典唱段、身段表演与唱腔韵味进行系统传授,吸引了大批年轻学员与戏曲爱好者参与。</p>
+    <p>下一步,相关部门将继续完善传承人档案与数字影像采集,让歌仔戏这一珍贵文化遗产在新时代焕发新的生机。</p>
+  `,
+}

+ 56 - 0
src/views/generic-list/GenericListView.vue

@@ -0,0 +1,56 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import DashboardLayout from '@/components/DashboardLayout.vue'
+
+import GenericListItem from './components/GenericListItem.vue'
+import { mockGenericList, type GenericListItem as GenericListItemData } from './data'
+
+const route = useRoute()
+const router = useRouter()
+
+// 输入参数:columId / mainBodyId / modelId / parentId
+const columnId = computed(() => route.query.columnId as string | undefined)
+const mainBodyId = computed(() => route.query.mainBodyId as string | undefined)
+const modelId = computed(() => route.query.modelId as string | undefined)
+const parentId = computed(() => route.query.parentId as string | undefined)
+
+// 后续替换为接口调用:根据 columId / mainBodyId / modelId / parentId 拉取列表
+const items = computed(() => mockGenericList)
+
+function onItemClick(item: GenericListItemData) {
+  router.push({
+    path: '/generic-content',
+    query: { modelId: modelId.value, id: item.id },
+  })
+}
+</script>
+
+<template>
+  <DashboardLayout subtitle="通用列表" footer-middle="通用列表页 · 数据驾驶舱">
+    <section class="mt-5 flex items-end justify-between border-b border-[#7f4034]/70 pb-4">
+      <div>
+        <h1 class="mt-2 text-[30px] font-bold tracking-[.16em] text-[#fff0bd]">通用列表</h1>
+      </div>
+      <div class="pb-1 text-xs text-[#c6a18c]">共 {{ items.length }} 条</div>
+    </section>
+
+    <section class="mt-4 border border-[#704033] bg-[#2b1514]/75 p-2 text-[11px] text-[#c6a18c]">
+      <span class="mr-4">columId:{{ columnId || '-' }}</span>
+      <span class="mr-4">mainBodyId:{{ mainBodyId || '-' }}</span>
+      <span class="mr-4">modelId:{{ modelId || '-' }}</span>
+      <span>parentId:{{ parentId || '-' }}</span>
+    </section>
+
+    <section class="mt-4 grid grid-cols-2 gap-3">
+      <GenericListItem v-for="item in items" :key="item.id" :item="item" @click="onItemClick" />
+    </section>
+
+    <p
+      v-if="items.length === 0"
+      class="mt-4 border-l-2 border-[#d0734e] bg-[#4a211d] px-3 py-2 text-xs text-[#f0d7a2]"
+    >
+      暂无数据。
+    </p>
+  </DashboardLayout>
+</template>

+ 38 - 0
src/views/generic-list/components/GenericListItem.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import type { GenericListItem } from '../data'
+
+defineProps<{ item: GenericListItem }>()
+
+defineEmits<{ (e: 'click', item: GenericListItem): void }>()
+</script>
+
+<template>
+  <button
+    type="button"
+    class="group flex gap-3 border border-[#704033] bg-[#351817] p-3 text-left transition hover:-translate-y-0.5 hover:border-[#d0734e] hover:bg-[#4a211d]"
+    @click="$emit('click', item)"
+  >
+    <span class="relative h-20 w-28 shrink-0 overflow-hidden border border-[#9b5a42] bg-[#64342b]">
+      <img
+        :alt="item.title"
+        class="h-full w-full object-cover opacity-75 transition duration-300 group-hover:scale-110 group-hover:opacity-100"
+        :src="item.image"
+      />
+      <span class="absolute inset-0 bg-gradient-to-t from-[#351817]/55 to-transparent"></span>
+    </span>
+    <span class="flex min-w-0 flex-1 flex-col justify-center">
+      <span class="block truncate font-bold text-[#f3dba1]">{{ item.title }}</span>
+      <span class="mt-1 block truncate text-[11px] text-[#d8b45e]">{{ item.subtitle }}</span>
+      <span class="mt-2 flex items-center gap-4 text-[11px] text-[#c6a18c]">
+        <span class="flex items-center gap-1">
+          <Icon icon="solar:calendar-mark-linear" width="13" />
+          {{ item.date }}
+        </span>
+        <span class="flex items-center gap-1">
+          <Icon icon="solar:user-id-linear" width="13" />
+          {{ item.author }}
+        </span>
+      </span>
+    </span>
+  </button>
+</template>

+ 73 - 0
src/views/generic-list/data.ts

@@ -0,0 +1,73 @@
+/**
+ * 通用列表页静态数据
+ *
+ * 说明:以下均为静态示例数据,用于前端页面搭建。
+ * 后续对接后端时,请根据入参 columnId / mainBodyId / modelId / parentId
+ * 调用列表接口,将 `mockGenericList` 替换为接口返回数据,
+ * 类型定义(interface)可保持不变,作为接口数据结构的约定。
+ */
+
+export interface GenericListItem {
+  id: string
+  /** 标题 */
+  title: string
+  /** 副标题 */
+  subtitle: string
+  /** 图片 */
+  image: string
+  /** 日期 */
+  date: string
+  /** 作者 */
+  author: string
+}
+
+export const mockGenericList: GenericListItem[] = [
+  {
+    id: '1',
+    title: '歌仔戏传承人在漳州古城开课',
+    subtitle: '传统戏剧 · 国家级非遗代表性项目',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/fec813433056449f94fa9a0c6344ce11.jpg',
+    date: '2026-08-18',
+    author: '漳州日报',
+  },
+  {
+    id: '2',
+    title: '漳州木版年画数字化采集完成',
+    subtitle: '传统美术 · 国家级非遗代表性项目',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/a1eb77553860452b88762b67954a1bec.jpg',
+    date: '2026-08-15',
+    author: '文化遗产保护中心',
+  },
+  {
+    id: '3',
+    title: '八宝印泥制作技艺口述史整理启动',
+    subtitle: '传统技艺 · 老字号档案',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/781e0f6f33674d8bbd86b466c2228bed.jpg',
+    date: '2026-08-12',
+    author: '非遗科',
+  },
+  {
+    id: '4',
+    title: '田螺坑土楼群建筑本体巡查纪实',
+    subtitle: '文化空间 · 传统村落保护',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/c53c5c35918d4cd88b31ca68702c240c.jpg',
+    date: '2026-08-09',
+    author: '南靖县文体旅局',
+  },
+  {
+    id: '5',
+    title: '东山歌册田野调查阶段性成果',
+    subtitle: '民间文学 · 省级非遗代表性项目',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/2a7798eae2754c6e87282c5677fc0aa8.jpg',
+    date: '2026-08-05',
+    author: '东山县文化馆',
+  },
+  {
+    id: '6',
+    title: '漳州元宵民俗档案辑录发布',
+    subtitle: '民俗档案 · 闽南文化档案',
+    image: 'https://modao.cc/agent-py/media/generated_images/2026-08-12/932c7ab061d045578f20f63e0defc2f5.jpg',
+    date: '2026-08-02',
+    author: '档案中心',
+  },
+]