快乐的梦鱼 3 months ago
commit
d0d8918bf1
100 changed files with 24182 additions and 0 deletions
  1. 23 0
      .gitignore
  2. 20 0
      index.html
  3. 11812 0
      package-lock.json
  4. 75 0
      package.json
  5. 10 0
      shims-uni.d.ts
  6. 38 0
      src/App.vue
  7. 428 0
      src/api/CommonContent.ts
  8. 12 0
      src/api/NotConfigue.ts
  9. 242 0
      src/api/RequestModules.ts
  10. 15 0
      src/api/Utils.ts
  11. 119 0
      src/api/auth/UserApi.ts
  12. 188 0
      src/api/inhert/VillageApi.ts
  13. 207 0
      src/api/inhert/VillageInfoApi.ts
  14. 59 0
      src/common/components/ImageWrapper.vue
  15. 29 0
      src/common/components/RequireLogin.vue
  16. 74 0
      src/common/components/SimpleDropDownPicker.vue
  17. 105 0
      src/common/components/SimplePageContentLoader.vue
  18. 27 0
      src/common/components/SimplePageListLoader.vue
  19. 90 0
      src/common/components/form/SimpleDynamicFormCate.vue
  20. 84 0
      src/common/components/form/SimpleDynamicFormCateInner.vue
  21. 232 0
      src/common/components/form/SimpleDynamicFormControl.vue
  22. 152 0
      src/common/components/form/SimpleDynamicFormUni.vue
  23. 37 0
      src/common/components/form/components/CityPicker.vue
  24. 45 0
      src/common/components/form/components/DynamicCheckbox.vue
  25. 45 0
      src/common/components/form/components/DynamicSelect.vue
  26. 63 0
      src/common/components/form/components/ImageUploaderWrapper.vue
  27. 38 0
      src/common/components/form/components/LonlatPicker.vue
  28. 50 0
      src/common/components/form/components/RichTextEditor.vue
  29. 42 0
      src/common/components/form/form/Form.vue
  30. 14 0
      src/common/components/form/form/FormItem.vue
  31. 159 0
      src/common/components/form/index.ts
  32. 363 0
      src/common/components/sunui-upimg/sunui-upimg.vue
  33. 17 0
      src/common/composeabe/ErrorDisplay.ts
  34. 42 0
      src/common/composeabe/LoadQuerys.ts
  35. 9 0
      src/common/composeabe/LoaderCommon.ts
  36. 26 0
      src/common/composeabe/RequireLogin.ts
  37. 64 0
      src/common/composeabe/SimpleDataLoader.ts
  38. 97 0
      src/common/composeabe/SimpleLocalDataStorage.ts
  39. 40 0
      src/common/composeabe/SimplePageContentLoader.ts
  40. 80 0
      src/common/composeabe/SimplePageListLoader.ts
  41. 10 0
      src/common/composeabe/SwiperImagePreview.ts
  42. 35 0
      src/common/composeabe/TabControl.ts
  43. 9 0
      src/common/config/ApiCofig.ts
  44. 23 0
      src/common/config/AppCofig.ts
  45. 7 0
      src/common/config/ImagesUrls.ts
  46. 969 0
      src/common/scss/common.scss
  47. 15 0
      src/common/scss/define/border-radius.scss
  48. 43 0
      src/common/scss/define/colors.scss
  49. 32 0
      src/common/scss/define/margin-padding.scss
  50. 61 0
      src/common/scss/define/size.scss
  51. 42 0
      src/common/scss/define/wing-height.scss
  52. 353 0
      src/common/scss/font.scss
  53. 4 0
      src/common/scss/font_num.scss
  54. 94 0
      src/common/scss/global/base.scss
  55. 55 0
      src/common/scss/global/border.scss
  56. 17 0
      src/common/scss/global/color.scss
  57. 138 0
      src/common/scss/global/flex.scss
  58. 3 0
      src/common/scss/global/grid.scss
  59. 347 0
      src/common/scss/global/margin-padding.scss
  60. 51 0
      src/common/scss/global/radius.scss
  61. 25 0
      src/common/scss/global/shadow.scss
  62. 80 0
      src/common/scss/global/size.scss
  63. 169 0
      src/common/scss/global/text.scss
  64. 71 0
      src/common/scss/global/wing-space-height.scss
  65. 8 0
      src/common/style/commonParserStyle.ts
  66. 122 0
      src/common/utils/ConvertRgeistry.ts
  67. 8 0
      src/env.d.ts
  68. 561 0
      src/libs/amap-wx.130.js
  69. 19 0
      src/main.ts
  70. 95 0
      src/manifest.json
  71. 159 0
      src/pages.json
  72. 106 0
      src/pages/article/editor/editor.vue
  73. 23 0
      src/pages/article/editor/preview.vue
  74. 23 0
      src/pages/dig/composeable/TaskEntryForm.ts
  75. 253 0
      src/pages/dig/details.vue
  76. 170 0
      src/pages/dig/forms/common.vue
  77. 2426 0
      src/pages/dig/forms/forms.ts
  78. 135 0
      src/pages/dig/forms/list.vue
  79. 26 0
      src/pages/dig/forms/success.vue
  80. 176 0
      src/pages/dig/forms/village_claim.vue
  81. 251 0
      src/pages/dig/index.vue
  82. 55 0
      src/pages/dig/task/building.vue
  83. 75 0
      src/pages/dig/task/custom.vue
  84. 46 0
      src/pages/dig/task/environment.vue
  85. 35 0
      src/pages/dig/task/food.vue
  86. 76 0
      src/pages/dig/task/history.vue
  87. 45 0
      src/pages/dig/task/mine.vue
  88. 69 0
      src/pages/dig/task/summary.vue
  89. 61 0
      src/pages/dig/task/trip.vue
  90. 550 0
      src/pages/index.vue
  91. 59 0
      src/pages/parts/Box2LineImageRightShadow.vue
  92. 145 0
      src/pages/parts/Box2LineLargeImageUserShadow.vue
  93. 9 0
      src/pages/parts/ContentNote.vue
  94. 59 0
      src/pages/parts/HomeTitle.vue
  95. 63 0
      src/pages/parts/ImageGrid.vue
  96. 46 0
      src/pages/parts/ImageSwiper.vue
  97. 52 0
      src/pages/parts/RoundTags.vue
  98. 100 0
      src/pages/parts/StatsText.vue
  99. 151 0
      src/pages/user/index.vue
  100. 0 0
      src/pages/user/login.vue

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+*.local
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+src/common/scss/global/base.css
+src/common/scss/global/base.css.map

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

File diff suppressed because it is too large
+ 11812 - 0
package-lock.json


+ 75 - 0
package.json

@@ -0,0 +1,75 @@
+{
+  "name": "uni-preset-vue",
+  "version": "0.0.0",
+  "scripts": {
+    "dev:custom": "uni -p",
+    "dev:h5": "uni",
+    "dev:h5:ssr": "uni --ssr",
+    "dev:mp-alipay": "uni -p mp-alipay",
+    "dev:mp-baidu": "uni -p mp-baidu",
+    "dev:mp-jd": "uni -p mp-jd",
+    "dev:mp-kuaishou": "uni -p mp-kuaishou",
+    "dev:mp-lark": "uni -p mp-lark",
+    "dev:mp-qq": "uni -p mp-qq",
+    "dev:mp-toutiao": "uni -p mp-toutiao",
+    "dev:mp-weixin": "uni -p mp-weixin",
+    "dev:mp-xhs": "uni -p mp-xhs",
+    "dev:quickapp-webview": "uni -p quickapp-webview",
+    "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
+    "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
+    "build:custom": "uni build -p",
+    "build:h5": "uni build",
+    "build:h5:ssr": "uni build --ssr",
+    "build:mp-alipay": "uni build -p mp-alipay",
+    "build:mp-baidu": "uni build -p mp-baidu",
+    "build:mp-jd": "uni build -p mp-jd",
+    "build:mp-kuaishou": "uni build -p mp-kuaishou",
+    "build:mp-lark": "uni build -p mp-lark",
+    "build:mp-qq": "uni build -p mp-qq",
+    "build:mp-toutiao": "uni build -p mp-toutiao",
+    "build:mp-weixin": "uni build -p mp-weixin",
+    "build:mp-xhs": "uni build -p mp-xhs",
+    "build:quickapp-webview": "uni build -p quickapp-webview",
+    "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
+    "build:quickapp-webview-union": "uni build -p quickapp-webview-union",
+    "type-check": "vue-tsc --noEmit"
+  },
+  "dependencies": {
+    "@dcloudio/uni-app": "3.0.0-4030620241128001",
+    "@dcloudio/uni-app-harmony": "3.0.0-4030620241128001",
+    "@dcloudio/uni-app-plus": "3.0.0-4030620241128001",
+    "@dcloudio/uni-components": "3.0.0-4030620241128001",
+    "@dcloudio/uni-h5": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-alipay": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-baidu": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-jd": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-kuaishou": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-lark": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-qq": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-toutiao": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
+    "@dcloudio/uni-mp-xhs": "3.0.0-4030620241128001",
+    "@dcloudio/uni-quickapp-webview": "3.0.0-4030620241128001",
+    "@imengyu/imengyu-utils": "^0.0.16",
+    "@imengyu/js-request-transform": "^0.3.3",
+    "@imengyu/vue-dynamic-form": "^0.1.1",
+    "async-validator": "^4.2.5",
+    "pinia": "^3.0.1",
+    "tslib": "^2.8.1",
+    "vue": "^3.4.21",
+    "vue-i18n": "^9.1.9"
+  },
+  "devDependencies": {
+    "@dcloudio/types": "^3.4.8",
+    "@dcloudio/uni-automator": "3.0.0-4030620241128001",
+    "@dcloudio/uni-cli-shared": "3.0.0-4030620241128001",
+    "@dcloudio/uni-stacktracey": "3.0.0-4030620241128001",
+    "@dcloudio/vite-plugin-uni": "3.0.0-4030620241128001",
+    "@vue/runtime-core": "^3.4.21",
+    "@vue/tsconfig": "^0.1.3",
+    "sass": "^1.86.0",
+    "typescript": "^4.9.4",
+    "vite": "5.2.8",
+    "vue-tsc": "^1.0.24"
+  }
+}

+ 10 - 0
shims-uni.d.ts

@@ -0,0 +1,10 @@
+/// <reference types='@dcloudio/types' />
+import 'vue'
+
+declare module '@vue/runtime-core' {
+  type Hooks = App.AppInstance & Page.PageInstance;
+
+  interface ComponentCustomOptions extends Hooks {
+
+  }
+}

+ 38 - 0
src/App.vue

@@ -0,0 +1,38 @@
+<style lang="scss">
+  @import "@/uni_modules/uview-plus/index.scss";
+	@import "@/common/scss/common.scss";
+	@import "@/common/scss/global/base.scss";
+</style>
+<script setup lang="ts">
+import { onLaunch } from '@dcloudio/uni-app'
+import { useAuthStore } from './store/auth'
+import AppConfig from '@/common/config/AppCofig'
+import { getCurrentPageUrl, navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
+
+const authStore = useAuthStore();
+
+
+onLaunch(async () => {
+  console.log('App Launch');
+  //加载登录信息。如果未登录,跳转登录页
+  if (!await authStore.loadLoginState()) {
+
+    const lastRedirectTime = uni.getStorageSync('lastRedirectTime') || 0;
+    if (Date.now() - lastRedirectTime < 50000)
+      return;
+    uni.setStorageSync('lastRedirectTime', Date.now());
+
+    setTimeout(() => {   
+      const pageUrl = getCurrentPageUrl() || '';
+      const noLoginPages = AppConfig.noLoginPages;
+      if (noLoginPages.indexOf('/' + pageUrl) == -1 && noLoginPages.indexOf(pageUrl) == -1)
+        navTo('/pages/user/login');
+    }, 1500);
+
+  }
+})
+</script>
+
+<style>
+	/*每个页面公共css */
+</style>

+ 428 - 0
src/api/CommonContent.ts

@@ -0,0 +1,428 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import ApiCofig from '@/common/config/ApiCofig';
+import { AppServerRequestModule } from './RequestModules';
+import { transformSomeToArray } from './Utils';
+import type { QueryParams } from '@imengyu/imengyu-utils/dist/request';
+
+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' },
+      publishAt: { 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'))
+        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 },
+      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' },
+      publishAt: { clientSide: 'date', serverSide: 'string' },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      type: { clientSide: 'number', serverSide: 'number' },
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time'))
+        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 ]
+      }
+    }
+  }
+
+  id = 0;
+  from = '';
+  modelId = 0;
+  type = 0;
+  title = '';
+  region = 0;
+  image = '';
+  images = [] as string[];
+  audio = '';
+  video = '';
+  desc = '';
+  flag ?: string[];
+  tags ?: string[];
+  views = 0;
+  comments = 0;
+  likes = 0;
+  collects = 0;
+  dislikes = 0;
+  isLogin = false;
+  isGuest = false;
+  isComment = false;
+  isLike = false;
+  isCollect = false;
+  content = '';
+  publishAt = new Date();
+  associationMeList = [] as {
+    id: number,
+    title: string,
+    image: string,
+    thumbnail: string,
+  }[];
+}
+
+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(
+    mainBodyId = ApiCofig.mainBodyId, 
+    modelId = 0, debugName = 'CommonContent', 
+    mainBodyColumnId?: number|number[]) {
+    super();
+    this.modelId = modelId;
+    this.mainBodyId = mainBodyId;
+    this.mainBodyColumnId = mainBodyColumnId;
+    this.debugName = debugName;
+  }
+
+  public mainBodyId: number;
+  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.data2, `获取分类列表`, 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.data2), 
+        `获取分类列表`, 
+        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('/content/content/getMainBodyColumnContentList', `${this.debugName} 主体栏目列表`, {
+      main_body_id: this.mainBodyId,
+      model_id: this.modelId,
+      ...params.toServerSide(),
+      ...querys,
+    })
+      .then(res => ({
+        list: transformArrayDataModel<T>(modelClassCreator, res.data2.list, `${this.debugName} 主体栏目列表`, true),
+        total: res.data2.total as number,
+      }))
+      .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('/content/content/getContentList', `${this.debugName} 模型内容列表`, {
+      ...params.toServerSide(),
+      model_id: params.modelId || this.modelId,
+      main_body_id: params.mainBodyId || this.mainBodyId,
+      main_body_column_id: this.toStringArray(params.mainBodyColumnId || this.mainBodyColumnId),
+      page,
+      pageSize,
+      ...querys,
+    })
+      .then(res => {
+        let resList : any = null;
+        let resTotal : any = null;
+
+        if (res.data2?.list && Array.isArray(res.data2.list)) {
+          resList = res.data2.list;
+          resTotal = res.data2.total ?? resList.length;
+        }
+        else if (res.data2 && Array.isArray(res.data2)) {
+          resList = res.data2;
+          resTotal = resList.length;
+        } else
+          resList = res.data;
+
+        if (resList === null)
+          return { list: [], total: 0 };
+        
+        return { 
+          list: transformArrayDataModel<T>(modelClassCreator, resList, `${this.debugName} 模型内容列表`, true),
+          total: resTotal as number,
+        }
+      })
+      .catch(e => { throw e });
+  }
+  /**
+   * 内容详情
+   * @param id id 
+   * @param querys 额外参数
+   * @returns 
+   */
+  getContentDetail<T extends DataModel = GetContentDetailItem>(id: number, modelClassCreator: NewDataModel = GetContentDetailItem, modelId?: number, querys?: QueryParams) {
+    return this.get('/content/content/getContentDetail', `${this.debugName} (${id}) 内容详情`, {
+      main_body_id: this.mainBodyId,
+      model_id: modelId ?? this.modelId,
+      id,
+      ...querys,
+    }, modelClassCreator)
+      .then(res => res.data as T)
+      .catch(e => { throw e });
+  }
+}
+
+export default new CommonContentApi(undefined, 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 = null;
+  }
+}
+
+export default new CommonApi();

+ 242 - 0
src/api/RequestModules.ts

@@ -0,0 +1,242 @@
+
+/**
+ * 这里写的是业务相关的:
+ * * 请求数据处理函数。
+ * * 自定义请求模块。
+ * * 自定义错误报告处理函数。
+ */
+
+import AppCofig, { isDev } from "../common/config/AppCofig";
+import ApiCofig from "@/common/config/ApiCofig";
+import uniappImplementer from "@imengyu/imengyu-utils/dist/request/implementer/Uniapp";
+import { appendGetUrlParams, appendPostParams } from "@imengyu/imengyu-utils/dist/request/utils/Utils";
+import { 
+  RequestCoreInstance, RequestOptions, RequestApiError, RequestApiResult, type RequestApiErrorType,
+  defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch,
+  RequestResponse,
+} from "@imengyu/imengyu-utils/dist/request";
+import type { DataModel, KeyValue, NewDataModel } from "@imengyu/js-request-transform";
+import { StringUtils } from "@imengyu/imengyu-utils";
+
+/**
+ * 不报告错误的 code
+ */
+const notReportErrorCode = [401] as number[];
+const notReportMessages = [
+  /请授权绑定手机号/g,
+] as RegExp[];
+function matchNotReportMessage(str: string) {
+  for (let i = 0; i < notReportMessages.length; i++) {
+    if (notReportMessages[i].test(str))
+      return true;
+  }
+  return false;
+}
+
+//请求拦截器
+function requestInceptor(url: string, req: RequestOptions) {
+  //获取store中的token,追加到头;
+  if (StringUtils.isNullOrEmpty((req.header as KeyValue).token as string)) {
+    const t = getApp()?.globalData?.token ?? '';
+    req.header['token'] = t
+    req.header['__token__'] = t;
+  }
+  const main_body_user_id = getApp()?.globalData?.userId ?? '';
+  const append_main_body_user_id = 
+    !(url.includes('content/content'));
+
+  if (req.method == 'GET') {
+    //追加GET参数
+    url = appendGetUrlParams(url, 'main_body_id', ApiCofig.mainBodyId);
+    if (append_main_body_user_id)
+      url = appendGetUrlParams(url, 'main_body_user_id', main_body_user_id);
+  } else {
+    req.data = appendPostParams(req.data,'main_body_id', ApiCofig.mainBodyId);
+    if (append_main_body_user_id)
+      req.data = appendPostParams(req.data,'main_body_user_id', main_body_user_id);
+  } 
+  return { newUrl: url, newReq: req };
+}
+//响应数据处理函数
+function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel|undefined, instance: RequestCoreInstance<T>, apiName: string | undefined): Promise<RequestApiResult<T>> {
+  return new Promise<RequestApiResult<T>>((resolve, reject) => {
+    const method = req.method || 'GET';
+    response.json().then((json) => {
+      if (response.ok) {
+        if (!json) {
+          reject(new RequestApiError(
+            'businessError',
+            '后端未返回数据',
+            '',
+            response.status,
+            null,
+            null,
+            req,
+            apiName,
+            response.url
+          ));
+          return;
+        }
+
+        //code == 0 错误
+        if (json.code === 0) {
+          handleError();
+          return;
+        }
+
+        //处理后端的数据
+        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;
+        }
+
+        resolve(new RequestApiResult(
+          resultModelClass ?? instance.config.modelClassCreator,
+          json?.code || response.status,
+          message,
+          data,
+          json
+        ));
+      }
+      else {
+        handleError();
+      }
+
+      function handleError() {
+        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;
+        }
+
+        reject(new RequestApiError(
+          errType,
+          errString,
+          errCodeStr,
+          response.status,
+          null,
+          null,
+          req,
+          apiName,
+          response.url
+        ));
+      }
+    }).catch((err) => {
+      //错误统一处理
+      defaultResponseDataHandlerCatch(method, req, response, null, err, apiName, response.url, reject, instance);
+    });
+  });
+}
+//错误报告处理
+function responseErrReoprtInceptor<T extends DataModel>(instance: RequestCoreInstance<T>, response: RequestApiError) {
+  return (
+    (response.errorType !== 'businessError' && response.errorType !== 'networkError') ||
+    notReportErrorCode.indexOf(response.code) >= 0 ||
+    matchNotReportMessage(response.errorMessage) === true
+  );
+}
+
+//错误报告处理
+export function reportError<T extends DataModel>(instance: RequestCoreInstance<T>, response: RequestApiError | Error) {
+  if (isDev) {
+    if (response instanceof RequestApiError) {
+      uni.showModal({
+        title: `请求错误 ${response.apiName} : ${response.errorMessage}`,
+        content: response.toString() +
+          '\r\n请求接口:' + response.apiName +
+          '\r\n请求地址:' + response.apiUrl +
+          '\r\n请求参数:' + JSON.stringify(response.rawRequest) +
+          '\r\n返回参数:' + JSON.stringify(response.rawData) +
+          '\r\n状态码:' + response.code +
+          '\r\n信息:' + response.errorCodeMessage,
+        type: 'error',
+        showCancel: false,
+      });
+    } else {
+      uni.showModal({
+        title: '错误报告 代码错误',
+        content: response?.stack || ('' + response),
+        type: 'error',
+        showCancel: false,
+      });
+    }
+  } else {    
+    let errMsg = '';
+    if (response instanceof RequestApiError)
+      errMsg = response.errorMessage + '。';
+      
+    errMsg += '服务出现了异常,请稍后重试或联系客服。';
+    errMsg += '版本:' + AppCofig.version;
+
+    uni.showModal({
+      title: '抱歉',
+      content: errMsg,
+      showCancel: false,
+    });
+}
+}
+
+/**
+ * App服务请求模块
+ */
+export class AppServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor() {
+    super(uniappImplementer);
+    this.config.baseUrl = ApiCofig.serverProd;
+    this.config.errCodes = []; //
+    this.config.requestInceptor = requestInceptor;
+    this.config.responseDataHandler = responseDataHandler;
+    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
+    this.config.reportError = reportError;
+  }
+}
+/**
+ * App服务请求模块
+ */
+export class AppServerRequestModule2<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor() {
+    super(uniappImplementer);
+    this.config.baseUrl = 'https://huli-app.wenlvti.net';
+    this.config.errCodes = []; //
+    this.config.requestInceptor = requestInceptor;
+    this.config.responseDataHandler = responseDataHandler;
+    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
+    this.config.reportError = reportError;
+  }
+}

+ 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;
+}

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

@@ -0,0 +1,119 @@
+import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import ApiCofig from '@/common/config/ApiCofig';
+import AppCofig from '@/common/config/AppCofig';
+
+
+export class LoginResult extends DataModel<LoginResult> {
+  constructor() {
+    super(LoginResult, "登录结果");
+    //this.setNameMapperCase('Camel', 'Snake');
+    this._beforeSolveServer = (data, self) => {
+      if (!data.userinfo && data.user)
+         data.userinfo = data.user;
+      if (!data.auth && data.userinfo)
+        data.auth = data.userinfo;
+      if (!data.mainBodyUserInfo && data.userinfo)
+        data.mainBodyUserInfo = data.userinfo;
+      return data;
+    };
+    this._convertTable = {
+      mainBodyUserInfo: { clientSide: 'object', clientSideRequired: true, clientSideChildDataModel: UserInfo },
+      auth: { 
+        clientSide: 'object', 
+        clientSideRequired: true, 
+        clientSideChildDataModel: {
+          nameMapperServer: {
+            user_id: 'userId',
+            expires_in: 'expiresIn',
+          },
+          convertTable: {
+            token: { clientSide: 'string', clientSideRequired: true },
+            userId: { clientSide: 'number' },
+            expiresIn: { clientSide: 'number' },
+          }
+        } 
+      },
+    }
+  }
+  auth !: {
+    id: number,
+    username: string,
+    nickname: string,
+    mobile: string,
+    avatar: string,
+    score: number,
+    token: string,
+    userId: number,
+    createtime: Date,
+    expiretime: Date,
+    expiresIn: number,
+  };
+  mainBodyUserInfo !:UserInfo;
+}
+export class UserInfo extends DataModel<UserInfo> {
+  constructor() {
+    super(UserInfo, "用户信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      userId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      mainBodyId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      loginTime: { clientSide: 'date', serverSide: 'string' },
+    }
+  }
+
+  id = 0;
+  userId = 0;
+  mainBodyId = 0;
+  nickname = '';
+  avatar = '';
+  intro = '';
+  fans = '';
+  score = '';
+  loginTime = null as null|Date;
+  diyname = '';
+}
+
+export class UserApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async loginThird(data?: {
+    code: string,
+    platform: 'wechat',
+    encrypted_data: string,
+    iv: string,
+    raw_data: string,
+    signature: string,
+  }) {
+    return (await this.post('/content/main_body_user/third', {
+      appid: AppCofig.appId,
+      main_body_id: 2, //AppCofig.mainBodyId,
+      ...data,
+    }, '登录', undefined, LoginResult)).data as LoginResult;
+  }
+  async loginWithMobile(data?: {
+    account: string,
+    password: string,
+  }) {
+    return (await this.post('/user/adminLogin', {
+      appid: AppCofig.appId,
+      account: data?.account,
+      password: data?.password,
+    }, '登录', 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();

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

@@ -0,0 +1,188 @@
+import { CONVERTER_ADD_DEFAULT, DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import { transformSomeToArray } from '../Utils';
+
+export class VillageListItem extends DataModel<VillageListItem> {
+  constructor() {
+    super(VillageListItem, "活动列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    this._nameMapperServer = {
+      name: 'villageName',
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+    this._afterSolveServer = () => {
+      this.address = 
+        (this.province || '') + 
+        (this.city || '') + 
+        (this.district || '') + 
+        (this.township || '');
+      if (this.images && this.images && this.images.length > 0  ) {
+        this.image = this.images[0]
+      }
+      this.thumbnail = this.image;
+      this.title = this.villageName
+    }
+
+  }
+
+  id !: number;
+  province = '' as string|null;
+  city = '' as string|null;
+  district = '' as string|null;
+  township = '' as string|null;
+  address = '';
+  villageVolunteerId = null as number|null;
+  villageId = null as number|null;
+  claimReason = '';
+  status = '';
+  statusText = '';
+  createdAt = null as Date|null;
+  updatedAt = null as Date|null;
+  deleteAt = null as Date|null;
+  image = '';
+  images = [] as string[];
+  villageName = '';
+  title = '';
+  volunteerName = '';
+}
+export class VolunteerRanklistItem extends DataModel<VolunteerRanklistItem> {
+  constructor() {
+    super(VolunteerRanklistItem, "活动列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      points: [{ clientSide: 'number', serverSide: 'number' }, { clientSide: CONVERTER_ADD_DEFAULT, clientSideParam: { defaultValue: 0 } }],
+      level: [{ clientSide: 'number', serverSide: 'number' }, { clientSide: CONVERTER_ADD_DEFAULT, clientSideParam: { defaultValue: 0 } }],
+    }
+  }
+
+  id !: number;
+  name = '';
+  mobile = '';
+  points = 0;
+  level = 0;
+  typeText = '';
+  sexText = '';
+  statusText = '';
+  image = '';
+}
+export class VolunteerInfo extends DataModel<VolunteerInfo> {
+  constructor() {
+    super(VolunteerInfo, "活动列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      points: [{ clientSide: 'number', serverSide: 'number' }, { clientSide: CONVERTER_ADD_DEFAULT, clientSideParam: { defaultValue: 0 } }],
+      level: [{ clientSide: 'number', serverSide: 'number' }, { clientSide: CONVERTER_ADD_DEFAULT, clientSideParam: { defaultValue: 0 } }],
+    }
+  }
+
+  id !: number;
+  mainBodyId !: number;
+  type = '';
+  name = '';
+  sex = 0;
+  mobile = '';
+  regionId = null as number|null;
+  address = '';
+  image = '';
+  birthday = new Date();
+  intro = '';
+  points = 0;
+  level = 0;
+  status = '';
+  createdAt = '';
+  updatedAt = '';
+  typeText = '';
+  sexText = '';
+  statusText = '';
+}
+
+export class VillageMenuListItem extends DataModel<VillageMenuListItem> {
+  constructor() {
+    super(VillageMenuListItem, "村落菜单列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    this._nameMapperServer = {
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+
+  }
+  name = '';
+  logo = '';
+}
+
+export class VillageApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async getClaimedVallageList(volunteerId?: string) {
+    return (this.get('/village/village/getVillageList', '获取已认领村落', {
+      village_volunteer_id: volunteerId,
+    })) 
+      .then(res => transformArrayDataModel<VillageListItem>(VillageListItem, transformSomeToArray(res.data2), `村落`, true))
+      .catch(e => { throw e });
+  }
+  async getCanClaimVallageList() {
+    return (this.get('/village/village/getClaimList', '可认领村落列表', {
+      main_body_id: 2,
+    })) 
+      .then(res => transformArrayDataModel<VillageListItem>(VillageListItem, transformSomeToArray(res.data2), `村落`, true))
+      .catch(e => { throw e });
+  }
+  async claimVallage(data: any) {
+    return (this.post('/village/village/addVillageClaim', {
+      ...data
+    }, '认领村落')) ;
+  }
+  async getVallageList(level?: number) {
+    return (this.get('/village/village/getList', '村落列表', {
+      history_level: level,
+    })) 
+      .then(res => transformArrayDataModel<VillageListItem>(VillageListItem, transformSomeToArray(res.data2), `村落`, true))
+      .catch(e => { throw e });
+  }
+  async getVolunteerInfo() {
+    return (await this.post('/village/volunteer/getInfo', {
+    }, '获取志愿者信息', undefined, VolunteerInfo)).data as VolunteerInfo
+  }
+  async getVolunteerRanklist(category?: number) {
+    return (this.post('/village/volunteer/getRanklist', {
+      category,
+    }, '志愿者排行榜')) 
+      .then(res => transformArrayDataModel<VolunteerRanklistItem>(VolunteerRanklistItem, res.data2, ``, true))
+      .catch(e => { throw e });
+  }
+  
+  async getVillageMenuList(id: number) {
+    return (this.get('/village/menu/getList', '村落菜单列表', {
+      village_id: id,
+    })) 
+      .then(res => transformArrayDataModel<VillageMenuListItem>(VillageMenuListItem, res.data2, `村落菜单`, true))
+      .catch(e => { throw e });
+  }
+
+}
+
+export default new VillageApi();

+ 207 - 0
src/api/inhert/VillageInfoApi.ts

@@ -0,0 +1,207 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import CommonContent from '../CommonContent';
+import { AppServerRequestModule } from '../RequestModules';
+
+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 CommonInfoModel extends DataModel<CommonInfoModel> {
+  constructor() {
+    super(CommonInfoModel, "信息详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+
+    },
+    this._afterSolveServer = () => {
+      if (this.province && this.city && this.district) {
+        this.cityAddress = [this.province as string, this.city as string, this.district as string];
+      }
+    };
+    this._afterSolveClient = (data) => {
+      if (this.cityAddress) {
+        data.province = this.cityAddress[0];
+        data.city = this.cityAddress[1];
+        data.district = this.cityAddress[2];
+      }
+    };
+  }
+  id !: number;
+  cityAddress?: string[];
+
+}
+
+export class VillageEnvInfo extends DataModel<VillageEnvInfo> {
+  constructor() {
+    super(VillageEnvInfo, "地理信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      landforms: [
+        { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+        { clientSide: 'arrayInt', serverSide: 'original' },
+      ],
+    },
+    this._afterSolveServer = () => {
+      if (this.longitude && this.latitude) {
+        this.lonlat = [this.longitude as number, this.latitude as number];
+      }
+    };
+    this._afterSolveClient = (data) => {
+      if (this.lonlat) {
+        data.longitude = this.lonlat[0];
+        data.latitude = this.lonlat[1];
+      }
+    };
+  }
+  id !: number;
+  lonlat?: number[];
+  landforms = [] as string[];
+}
+export class VillageListItem extends DataModel<VillageListItem> {
+  constructor() {
+    super(VillageListItem, "村庄信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    },
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+    this._afterSolveServer = () => {
+      if (!this.title) {
+        if (this.name) this.title = this.name as string;
+        if (typeof this.content === 'object' && (this.content as any)?.title) this.title = (this.content as any).title as string;
+        if (this.content) this.title = this.content as string;
+        if (this.structure) this.title = this.structure as string;
+        if (this.wisdom) this.title = this.wisdom as string;
+      }
+      if (!this.image) {
+        if (this.distribution) this.image = this.distribution as string;
+      }
+    };
+  }
+  id !: number;
+  createdAt = new Date();
+  updatedAt = new Date();
+  title = '';
+  image = '';
+}
+export class VillageBulidingInfo extends DataModel<VillageBulidingInfo> {
+  constructor() {
+    super(VillageBulidingInfo, "历史建筑信息");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    const commaArrayKeys = [
+      'purpose','floorType','wallType','roofForm','bearingType',
+    ]
+    this._convertKeyType = (key, direction) => {
+      if (commaArrayKeys.includes(key))
+        return [
+          { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+          { clientSide: 'arrayInt', serverSide: 'original' },
+        ];
+      return undefined;
+    };
+  }
+  id !: number;
+}
+
+export class VillageInfoApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * 获取分类列表
+   * @param type 根级类型:1=区域、2=级别、3=文物类型、4=非遗类型、42=事件类型
+   * @param withself 是否返回包含自己:true=是,false=否 ,默认false
+   * @returns 
+   */
+  getCategoryList(
+    type?: number,
+    withself?: boolean,
+  ) {
+    return CommonContent.getCategoryList(type, withself);
+  }
+  /**
+   * 用于获取某一个分类需要用的子级
+   * @param pid 父级
+   * @returns 
+   */
+  getCategoryChildList(pid?: number) {
+    return CommonContent.getCategoryChildList(pid);
+  }
+
+  async getInfo<T extends DataModel>(
+    sub: string,
+    subId: number,
+    villageId: number,
+    villageVolunteerId: number,
+    id?: number,
+    modelClassCreator: (new () => T) = CommonInfoModel as any
+  ) {
+    return (await this.post(`/village/${sub}/getInfo`, {
+      type: subId,
+      village_id: villageId,
+      village_volunteer_id: villageVolunteerId,
+      id,
+    }, '获取信息详情', undefined, modelClassCreator)).data as T
+  }
+  async getList<T extends DataModel = VillageListItem>(
+    sub: string,
+    subId: number|undefined,
+    subKey: string|undefined,
+    villageId: number,
+    villageVolunteerId: number,
+    modelClassCreator: (new () => T) = VillageListItem as any 
+  ) {
+    return (this.post(`/village/${sub}/getList`, {
+      [subKey ? subKey : 'type']: subId,
+      village_id: villageId,
+      village_volunteer_id: villageVolunteerId,
+    }, '获取信息详情'))
+      .then(res => transformArrayDataModel<T>(modelClassCreator, res.data2, `获取分类列表`, true))
+      .catch(e => { throw e });
+  }
+  async updateInfo<T extends DataModel>(
+    sub: string,
+    villageId: number,
+    villageVolunteerId: number,
+    data: T,
+  ) {
+    return (await this.post(`/village/${sub}/save`, {
+      sub,
+      village_id: villageId,
+      village_volunteer_id: villageVolunteerId,
+      ...data.toServerSide(),
+    }, '更新信息详情'));
+  }
+}
+
+export default new VillageInfoApi();

+ 59 - 0
src/common/components/ImageWrapper.vue

@@ -0,0 +1,59 @@
+<template>
+  <view class="image-wrapper">
+    <u-image 
+      :showLoading="true"
+      v-bind="$props"
+      :src="src || EmptyImage"
+      :style="{
+        width: width + 'px',
+        height: height + 'px',
+      }"
+      @click="onClick"
+    >
+      <template #loading>
+        <u-loading-icon color="red"></u-loading-icon>
+      </template>
+      <template #error>
+        <u-empty mode="page" text="图片加载失败" />
+      </template>
+    </u-image>
+  </view>
+</template>
+
+<script setup lang="ts">
+import EmptyImage from '@/static/EmptyImage.png';
+
+const props = defineProps({	
+  src: {
+    type: String,
+    required: true,
+  },
+  mode: {
+    type: String,
+    default: 'aspectFill',
+  },
+  canPreview: {
+    type: Boolean,
+    default: false,
+  },
+  width: {
+    type: [Number,String],
+    default: 0,
+  },
+  height: {
+    type: [Number,String],
+    default: 0,
+  },
+})
+
+const emit = defineEmits([ 'clcik' ])
+
+function onClick() {
+  if (props.canPreview) {
+    uni.previewImage({
+      urls: [props.src],
+    });
+  } 
+  emit('clcik')
+}
+</script>

+ 29 - 0
src/common/components/RequireLogin.vue

@@ -0,0 +1,29 @@
+<template>
+  <slot v-if="isLogged" />
+  <view v-else class="d-flex flex-column align-center justify-center height-300">
+    <view class="mb-3">
+      <text>{{unLoginMessage}}</text>
+    </view>
+    <u-button class="w-50" type="primary" @click="goLogin">去登录</u-button>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useAuthStore } from '@/store/auth';
+import { computed } from 'vue';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+
+const authStore = useAuthStore();
+const isLogged = computed(() => authStore.isLogged);
+
+defineProps({	
+  unLoginMessage : {
+    type: String,
+    default: '登录后享受更多权益'
+  },
+})
+
+function goLogin() {
+  navTo('/pages/user/login');
+}
+</script>

+ 74 - 0
src/common/components/SimpleDropDownPicker.vue

@@ -0,0 +1,74 @@
+<template>
+  <view 
+    class="simple-dropdown-box" 
+    @click="show=true"
+  >
+    <text v-if="preText" class="preText">{{ preText }}</text>
+    {{ dispayText }} ▼
+  </view>
+  <u-picker 
+    :show="show" 
+    :columns="[columns]" 
+    keyName="name"
+    @cancel="show=false"
+    @confirm="confirm"
+  />
+</template>
+
+<script setup lang="ts">
+import { computed, ref, type PropType } from 'vue';
+
+export interface SimpleDropDownPickerItem {
+  id: number,
+  name: string,
+}
+
+const props = defineProps({	
+  columns: {
+    type: Object as PropType<SimpleDropDownPickerItem[]>,
+    default: null,
+  },
+  modelValue: {
+    type: Number,
+    default: null,
+  },
+  defaultText: {
+    type: String,
+    default: '请选择',
+  },
+  preText: {
+    type: String,
+    default: '',
+  },
+})
+
+const emit = defineEmits([
+  'update:modelValue', 
+])
+
+const show = ref(false);
+const dispayText = computed(() => {
+  if (props.modelValue) 
+    return props.columns.find(item => item.id == props.modelValue)?.name;
+  return props.defaultText;
+});
+
+function confirm(e: { value: SimpleDropDownPickerItem[] }) {
+  show.value = false;
+  emit('update:modelValue', e.value[0].id);
+}
+</script>
+
+<style lang="scss">
+.simple-dropdown-box {
+  position: relative;
+  padding: 16rpx 18rpx;
+  border-radius: 20rpx;
+  background: #FFFFFF;
+
+  .preText {
+    margin-right: 10rpx;
+    color: #525252;
+  }
+}
+</style>

+ 105 - 0
src/common/components/SimplePageContentLoader.vue

@@ -0,0 +1,105 @@
+<template>
+  <view
+    v-if="loader?.loadStatus.value == 'loading'"
+    style="min-height: 200rpx;display: flex;justify-content: center;align-items: center;"
+  >
+    <u-loading-icon text="加载中" textSize="18" />
+  </view>
+  <view
+    v-else-if="loader?.loadStatus.value == 'error'"
+    style="min-height: 200rpx"
+  >
+    <u-empty
+      mode="page"
+      :text="loader.loadError.value"
+    />
+    <view style="margin-top: 20rpx">
+      <u-row justify="center">
+        <u-col span="3">
+          <u-button text="刷新" @click="handleRetry" />
+        </u-col>
+      </u-row>
+    </view>
+  </view>
+  <template v-else-if="loader?.loadStatus.value == 'finished' || loader?.loadStatus.value == 'nomore'">
+    <slot />
+  </template>
+  <view
+    v-if="showEmpty || loader?.loadStatus.value == 'nomore'"
+    style="min-height: 200rpx"
+  >
+    <u-empty
+      mode="data"
+      :text="emptyView?.text ?? '暂无数据'"
+    />
+    <view v-if="emptyView?.button" style="margin-top: 20rpx">
+      <u-row justify="center">
+        <u-col span="3">
+          <u-button
+            :text="emptyView?.buttonText ?? '刷新'" 
+            @click="() => emptyView?.buttonClick ? emptyView?.buttonClick() : handleRetry()"
+          />
+        </u-col>
+      </u-row>
+    </view>
+  </view>
+  <image 
+    v-if="lazy && !loaded"
+    :lazy-load="true"
+    @load="handleLoad"
+    @error="handleLoad"
+    src="https://mn.wenlvti.net/uploads/20250313/46adb2f039c6f23a3e69149526eb7e61.png"
+    style="width:0px;height:0px"
+  />
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, type PropType } from 'vue';
+import type { ISimplePageContentLoader } from '../composeabe/SimplePageContentLoader';
+
+const props = defineProps({	
+  loader: {
+    type: Object as PropType<ISimplePageContentLoader<any, any>>,
+    default: null,
+  },
+  lazy: {
+    type: Boolean,
+    default: false, 
+  },
+  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>

+ 27 - 0
src/common/components/SimplePageListLoader.vue

@@ -0,0 +1,27 @@
+<template>
+  <u-loadmore 
+    v-if="
+    loader.loadStatus.value == 'loading' 
+    || (loader.loadStatus.value == 'nomore' && !$slots.empty)" 
+    :status="loader.loadStatus.value" 
+  />
+  <slot v-else-if="loader.loadStatus.value == 'nomore' && $slots.empty" name="empty" />
+  <u-loadmore v-else-if="loader.loadStatus.value == 'error'" status="loadmore" :loadmoreText="loader.loadError.value" @loadmore="handleRetry" />
+
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+import type { ISimplePageListLoader } from '../composeabe/SimplePageListLoader';
+
+const props = defineProps({	
+  loader: {
+    type: Object as PropType<ISimplePageListLoader<any, any>>,
+    default: null,
+  },
+})
+
+function handleRetry() {
+  props.loader.loadData();
+}
+</script>

+ 90 - 0
src/common/components/form/SimpleDynamicFormCate.vue

@@ -0,0 +1,90 @@
+<template>
+  <view 
+    v-if="formDefine.type === 'group'" 
+    :class="`form-group ${formDefine.props.type}`"
+  >
+    <text class="form-group-title" v-if="formDefineParentLabel">
+      {{ formDefineParentLabel }}
+    </text>
+    <SimpleDynamicFormCateInner
+      :formDefine="formDefine"
+      :formModel="formModel" 
+      :groupType="formDefine.props.type"
+    />
+  </view>
+  <SimpleDynamicFormCateInner
+    v-else
+    :formDefine="formDefine"
+    :formModel="formModel" 
+  />
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+import type { FormDefine } from '.';
+import SimpleDynamicFormCateInner from './SimpleDynamicFormCateInner.vue';
+
+export interface FormGroupProps {
+  type: 'row' | 'column' | 'block';
+
+}
+
+const props = defineProps({
+  formDefineParentLabel: {
+    type: null,
+    default: '' 
+  },
+  formDefineParentKey: {
+    type: String,
+    default: '' 
+  },
+  formModel: {
+    type: Object,
+    default: () => ({})
+  },
+  formDefine: {
+    type: Object as PropType<FormDefine>,
+    default: () => ({})
+  },
+})
+</script>
+
+<style lang="scss">
+.form-group {
+  display: flex;
+  flex-direction: column;
+
+  &.block {
+    margin-bottom: 32rpx;
+    padding: 24rpx 26rpx;
+    background: #fff;
+    border-radius: 10rpx;
+
+    .form-group-title {
+      display: block;
+      font-size: 28rpx;
+      color: #333;
+      margin-bottom: 16rpx;
+    }
+  }
+
+  .form-group-title {
+    display: block;
+    flex-shrink: 0;
+    font-size: 28rpx;
+    color: #333;
+    margin-bottom: 16rpx;
+  }
+
+  &.row {
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+
+    .form-group-title {
+      display: inline-block;
+      margin-left: 10rpx;
+    }
+  }
+}
+</style>

+ 84 - 0
src/common/components/form/SimpleDynamicFormCateInner.vue

@@ -0,0 +1,84 @@
+<template>
+  <view 
+    v-for="(item, key) in formDefine.items"
+    :key="key"
+    :class="[
+      'form-cate-inner',
+      groupType
+    ]"
+  >
+    <SimpleDynamicFormCate
+      v-if="item.children"
+      :formDefine="item.children"
+      :formModel="children" 
+      :formDefineParentKey="item.name"
+      :formDefineParentLabel="item.label"
+      :parentModel="formModel"
+      :topModel="topModel"
+    />
+    <SimpleDynamicFormControl
+      v-else
+      :modelValue="formModel[item.name] ?? null"
+      :formDefineItem="item"
+      :parentModel="formModel"
+      :topModel="topModel"
+      @update:modelValue="(v: any) => formModel[item.name] = v"
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, type PropType } from 'vue';
+import type { FormDefine } from '.';
+import SimpleDynamicFormControl from './SimpleDynamicFormControl.vue';
+import SimpleDynamicFormCate from './SimpleDynamicFormCate.vue';
+
+const props = defineProps({	
+  topModel: {
+    type: Object,
+    default: () => ({})
+  },
+  parentModel: {
+    type: null,
+  },
+  formModel: {
+    type: Object,
+    default: () => ({})
+  },
+  formDefineParentKey: {
+    type: String,
+    default: ''
+  },
+  formDefine: {
+    type: Object as PropType<FormDefine>,
+    default: () => ({})
+  },
+  groupType: {
+    type: String,
+    default: '' 
+  }
+})
+
+const children = computed(() => {
+  if (props.formDefineParentKey && props.formDefine.propNestType == 'nest')
+    return props.formModel[props.formDefineParentKey];
+  return props.formModel;
+});
+
+</script>
+
+<style lang="scss">
+.form-cate-inner {
+  display: flex;
+  flex-direction: column;
+
+  &.row {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+}
+.form-static-text {
+  margin: 0 10rpx 20rpx 10px;
+}
+</style>

+ 232 - 0
src/common/components/form/SimpleDynamicFormControl.vue

@@ -0,0 +1,232 @@
+<template>
+  <template v-if="show">
+    <text
+      v-if="formDefineItem.type === 'static-text' "
+      class="form-static-text"
+      :style="(params.style as any)"
+      :class="(params.class as any)"
+    >
+      {{ params?.text ?? modelValue ?? null }}
+    </text>
+    <uni-forms-item 
+      v-else
+      ref="formItemRef"
+      :label="label"
+      :name="formDefineItem.fullName"
+      :required="Boolean(formDefineItem.rules?.length)"
+      v-bind="formDefineItem.itemParams"
+    >
+      <!-- <text>fullName: {{formDefineItem.fullName}}</text> -->
+      <template v-if="formDefineItem.type === 'text'">
+        <uni-easyinput 
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          :maxlength="260"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'number'">
+        <uni-number-box
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'radio'">
+        <uni-data-checkbox
+          ref="itemRef"
+          selectedColor="#ff8719"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'select'">
+        <uni-data-select 
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'checkbox'">
+        <uni-data-checkbox 
+          ref="itemRef"
+          selectedColor="#ff8719"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'bool-checkbox'"> 
+        <uni-data-checkbox 
+          ref="itemRef"
+          selectedColor="#ff8719"
+          :modelValue="modelValue"
+          :multiple="false"
+          :localdata="[{text: '是', value: true}, {text: '否', value: false}]"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'boolint-checkbox'"> 
+        <uni-data-checkbox 
+          ref="itemRef"
+          selectedColor="#ff8719"
+          :modelValue="modelValue"
+          :multiple="false"
+          :localdata="[{text: '是', value: 1}, {text: '否', value: 0}]"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'dynamic-checkbox'">
+        <DynamicCheckbox
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'dynamic-select'">
+        <DynamicSelect
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'city-select'">
+        <CityPicker
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'picker'">
+        <uni-data-picker
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'lonlat-picker'">
+        <LonlatPicker
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="(v:any) =>{onValueChanged(v);formItemRef.onFieldChange(v)}"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'textarea'">
+        <uni-easyinput 
+          ref="itemRef"
+          type="textarea" 
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'richtext'">
+        <RichTextEditor
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'datetime-picker'">
+        <uni-datetime-picker
+          ref="itemRef"
+          :value="modelValue"
+          v-bind="params"
+          @change="(e: any) => onValueChanged(e)"
+        />
+      </template>
+      <template v-else-if="formDefineItem.type === 'image-uploader'">
+        <ImageUploaderWrapper
+          ref="itemRef"
+          :value="modelValue"
+          v-bind="params"
+          @change="(e: any) => onValueChanged(e)"
+        />
+      </template>
+      <!-- More components can be added here... -->
+      <template v-else>
+        <text>Fallback: unknow form type {{ formDefineItem.type }}</text>
+      </template>
+    </uni-forms-item>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { computed, inject, onBeforeUnmount, onMounted, ref, type PropType } from 'vue';
+import type { FormDefineItem, IFormItemCallback } from '.';
+import DynamicSelect from './components/DynamicSelect.vue';
+import CityPicker from './components/CityPicker.vue';
+import LonlatPicker from './components/LonlatPicker.vue';
+import DynamicCheckbox from './components/DynamicCheckbox.vue';
+import RichTextEditor from './components/RichTextEditor.vue';
+import ImageUploaderWrapper from './components/ImageUploaderWrapper.vue';
+
+const props = defineProps({	
+  parentModel: {
+    type: null, //TODO: parentModel
+  },
+  modelValue: {
+    type: null
+  },
+  formDefineItem: {
+    type: Object as PropType<FormDefineItem>,
+    default: () => ({})
+  },
+});
+
+const formItemRef = ref();
+const topModel = inject<any>('formTopModel', {});
+const formGlobalParams = inject<any>('formGlobalParams', {});
+
+function evaluateCallback(val: unknown|IFormItemCallback<unknown>) {
+  if (typeof val === 'object' && typeof (val as IFormItemCallback<unknown>).callback === 'function')
+    return (val as IFormItemCallback<unknown>).callback(
+      props.modelValue, 
+      topModel.value, 
+      props.parentModel, 
+      formGlobalParams.value,
+      props.formDefineItem,
+    );
+  return val as unknown;
+}
+function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unknown>>) {
+  const newObj = {} as Record<string, unknown>;
+  for (const key in val) {
+    if (Object.prototype.hasOwnProperty.call(val, key))
+      newObj[key] = evaluateCallback(val[key]);
+  }
+  return newObj;
+}
+
+const params = computed(() => evaluateCallbackObj(props.formDefineItem.params as any))
+const label = computed(() => evaluateCallback(props.formDefineItem.label))
+const show = computed(() => props.formDefineItem.show == undefined || evaluateCallback(props.formDefineItem.show))
+
+const itemRef = ref();
+const emit = defineEmits([ 'update:modelValue' ]);
+ 
+function onValueChanged(v: any) {
+  props.formDefineItem.onChange?.(props.modelValue, v, topModel.value, itemRef.value);
+  emit('update:modelValue', v);
+}
+
+onMounted(() => {
+  props.formDefineItem.onMounted?.(topModel.value, itemRef.value);
+})
+onBeforeUnmount(() => {
+  props.formDefineItem.onBeforeUnMount?.(topModel.value, itemRef.value); 
+})
+
+</script>

+ 152 - 0
src/common/components/form/SimpleDynamicFormUni.vue

@@ -0,0 +1,152 @@
+<template>
+  <uni-forms 
+    ref="formRef"
+    v-bind="formProps"
+    :model="formModel"
+    :rules="formRules"
+  >
+    <SimpleDynamicFormCate
+      v-if="formModel"
+      :formModel="formModel"
+      :formDefine="formDefine"
+      :formDefineParentKey="''"
+      :formDefineParentLabel="''"
+    />
+  </uni-forms>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, provide, reactive, ref, toRef, watch, type PropType } from 'vue';
+import { toast } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
+import type { FormDefine, FormDefineItem, FormExport } from '.';
+import SimpleDynamicFormCate from './SimpleDynamicFormCate.vue';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
+
+const props = defineProps({	
+  formDefine: {
+    type: Object as PropType<FormDefine>,
+    default: () => ({})
+  },
+  formProps: {
+    type: Object,
+    default: () => ({}) 
+  },
+  formGlobalParams: {
+    type: Object,
+    default: () => ({})
+  },
+});
+
+const formRef = ref<any>();
+const formModel = ref<any>(null);
+const formRules = computed(() => {
+  const rules: Record<string, any> = {};
+  function loop(prevKey: string, arr: FormDefineItem[]) {
+    if (!arr || !(arr instanceof Array))
+     return;
+    for (const item of arr) {
+      const key = prevKey ? `${prevKey}.${item.name}` : item.name;
+      if (key)
+        rules[key] = { 
+          label: item.label,
+		      validateTrigger: 'submit',
+          rules: item.rules
+        };
+      if (item.children) {
+        loop(
+          item.children.propNestType === 'flat' ? key : prevKey, 
+          item.children.items
+        );
+      }
+    }
+  }
+  loop('', props.formDefine.items);
+  return rules;
+});
+const formGlobalParams = toRef(props.formGlobalParams);
+
+provide('formTopModel', formModel);
+provide('formGlobalParams', formGlobalParams);
+
+watch(formRules, (v) => {
+  formRef.value?.setRules(v);
+});
+watch(() => props.formDefine, (v) => {
+  reloadFormData();
+});
+
+let isErrorState = false;
+let initCb : () => any = () => {
+  return {};
+};
+
+function initFormData(data: () => any) {
+  initCb = data;
+}
+function loadFormData(value?: Record<string, any>) {
+  const obj = reactive(initCb());
+
+  function loop(prevKey: string, arr: FormDefineItem[]) {
+    if (!arr || !(arr instanceof Array))
+     return;
+    for (let index = 0; index < arr.length; index++) {
+      const item = arr[index];
+      const key = prevKey ? `${prevKey}.${item.name}` : item.name;
+      if (key) {
+        const valueProvided = value?.[key] ;
+        obj[key] = valueProvided == null || valueProvided == undefined ? 
+          (typeof item.defaultValue === 'function' ? item.defaultValue() : item.defaultValue)  
+          : valueProvided ?? null;
+        item.fullName = key;
+      } else {
+        item.fullName = '';
+      }
+      if (item.children)
+        loop(
+          item.children.propNestType === 'flat' ? key : prevKey, 
+          item.children.items
+        );
+    }
+  }
+
+  loop('', props.formDefine.items);
+  formModel.value = obj;
+}
+async function submitForm<T = Record<string, any>>() : Promise<T|null> {
+  await formRef.value.clearValidate();
+  await waitTimeOut(50);
+  
+  try {
+    await formRef.value.validate();
+  } catch (e) {
+    if (isErrorState)
+      toast('请将表单填写完整');
+    console.log(e);
+    isErrorState = true;
+    return null;
+  }
+  isErrorState = false;
+  return formModel.value;
+}
+function resetForm() {
+  loadFormData();
+}
+
+function reloadFormData() {
+  if (!formModel.value)
+    loadFormData();
+  formRef.value.setRules(formRules.value);
+}
+
+onMounted(() => {
+  setTimeout(() => reloadFormData(), 300);
+});
+
+defineExpose<FormExport>({
+  initFormData,
+  loadFormData,
+  submitForm,
+  resetForm,
+})
+
+</script>

+ 37 - 0
src/common/components/form/components/CityPicker.vue

@@ -0,0 +1,37 @@
+<template>
+  <uni-data-picker
+    :modelValue="modelValue"
+    :localdata="data"
+    :map="{ text: 'text', value: useCode ? 'value' : 'text' }"
+    @change="onChange"
+  >
+  </uni-data-picker>
+</template>
+
+<script setup lang="ts">
+import NotConfigue from '@/api/NotConfigue';
+import { onMounted, ref } from 'vue';
+
+const data = ref();
+const props = defineProps({	
+  modelValue: { 
+    type: Array,
+    default: null 
+  },
+  useCode: {
+    type: Boolean,
+    default: false,
+  },
+})
+const emit = defineEmits(['update:modelValue'])
+
+onMounted(() => {
+  NotConfigue.get('https://mn.wenlvti.net/app_static/xiangan/city-data.json', '', undefined).then((res) => {
+    data.value = res.data; 
+  })
+});
+
+function onChange(e: any) {
+  emit('update:modelValue', e.detail.value.map((x: any) => props.useCode ? x.value : x.text));
+}
+</script>

+ 45 - 0
src/common/components/form/components/DynamicCheckbox.vue

@@ -0,0 +1,45 @@
+<template>
+  <u-loading-icon v-if="data2.loadStatus.value === 'loading'" />
+  <view 
+    v-else-if="data2.loadStatus.value === 'error'" 
+    class="d-flex flex-row align-center"
+    @click="data2.loadData(undefined, true)"
+  >
+    <u-icon name="error-circle-fill"></u-icon>
+    <text class="ml-2">{{ data2.loadError.value }}</text>
+  </view>
+  <uni-data-checkbox
+    v-else
+    :modelValue="modelValue"
+    selectedColor="#ff8719"
+    @update:modelValue="(v: any) => $emit('update:modelValue', v)"
+    :localdata="data2.content.value"
+    v-bind="$attrs"
+  >
+  </uni-data-checkbox>
+</template>
+
+<script setup lang="ts">
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import type { PropType } from 'vue';
+
+export interface DynamicCheckboxProps {
+  loadData: () => Promise<{
+    text: string
+    value: any
+    disable?: boolean
+  }[]>;
+}
+
+const props = defineProps({	
+  modelValue : { type: null }	,
+  loadData: { 
+    type: Function as PropType<DynamicCheckboxProps['loadData']>, 
+    default: () => {} 
+  },
+})
+const data2 = useSimpleDataLoader(props.loadData, true);
+
+defineEmits(['update:modelValue'])
+
+</script>

+ 45 - 0
src/common/components/form/components/DynamicSelect.vue

@@ -0,0 +1,45 @@
+<template>
+  <uni-data-select 
+    :modelValue="modelValue"
+    @update:modelValue="(v: any) => $emit('update:modelValue', v)"
+    :localdata="data2.content.value"
+    v-bind="$attrs"
+  >
+    <template #prefix>
+      <u-loading-icon v-if="data2.loadStatus.value === 'loading'" />
+      <view 
+        v-else-if="data2.loadStatus.value === 'error'" 
+        class="d-flex flex-row align-center"
+        @click="data2.loadData(undefined, true)"
+      >
+        <u-icon name="error-circle-fill"></u-icon>
+        <text class="ml-2">{{ data2.loadError.value }}</text>
+      </view>
+    </template>
+  </uni-data-select>
+</template>
+
+<script setup lang="ts">
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import type { PropType } from 'vue';
+
+export interface DynamicSelectProps {
+  loadData: () => Promise<{
+    text: string
+    value: any
+    disable?: boolean
+  }[]>;
+}
+
+const props = defineProps({	
+  modelValue : { type: null }	,
+  loadData: { 
+    type: Function as PropType<DynamicSelectProps['loadData']>, 
+    default: () => {} 
+  },
+})
+const data2 = useSimpleDataLoader(props.loadData, true);
+
+defineEmits(['update:modelValue'])
+
+</script>

+ 63 - 0
src/common/components/form/components/ImageUploaderWrapper.vue

@@ -0,0 +1,63 @@
+<template>
+  <sunui-upimg 
+    ref="upload"
+    title="上传图片"
+    :url="upPicUrl"
+    :before-upload="onBeforeUpload" 
+    :header="headers"
+    @change="handleChange"
+    v-bind="$attrs"
+  />
+</template>
+
+<script setup lang="ts">
+import SunuiUpimg from '@/common/components/sunui-upimg/sunui-upimg.vue';
+import { toast } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
+import { computed, nextTick, onMounted, ref, watch } from 'vue';
+import { useAuthStore } from '@/store/auth';
+import { RequestApiConfig } from '@imengyu/imengyu-utils/dist/request';
+
+const props = defineProps({
+  modelValue: {
+    type: Array,
+    default: null
+  },
+})
+
+const authStore = useAuthStore();
+const headers = computed(() => ({
+  token: authStore.token,
+}));
+const upPicUrl = RequestApiConfig.getConfig().BaseUrl + '/common/upload';
+const upload = ref();
+const emit = defineEmits(['update:modelValue'])
+
+function onBeforeUpload(file: any, index: number) {
+  let fileType = ['image/png', 'image/jpeg'];
+
+  if (file.size / 1000 > 51.2) {
+    toast('图片大小不能大于50K');
+    return false;
+  }
+  if (fileType.indexOf(file.type) === -1) {
+    toast(`仅支持${fileType.join('、').replace(/image\//g, '')}图片格式`);
+    return false;
+  }
+}
+function handleChange(e: any) {
+  emit('update:modelValue', props.modelValue instanceof Array ? e : (e[0] || ''));
+}
+
+watch(() => props.modelValue, (v) => {
+  nextTick(() => {
+    upload.value.setItems(props.modelValue instanceof Array ? props.modelValue : [ props.modelValue ]);
+  })
+})
+
+onMounted(() => {
+  if (props.modelValue)
+    setTimeout(() => {
+      upload.value?.setItems(props.modelValue instanceof Array ? props.modelValue : [ props.modelValue ]);
+    }, 1000);
+})
+</script>

+ 38 - 0
src/common/components/form/components/LonlatPicker.vue

@@ -0,0 +1,38 @@
+<template>
+  <u-button 
+    type="primary"
+    :plain="true"
+    :text="dispayText"
+    @click="onPick"
+  >
+  
+  </u-button>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+
+const props = defineProps({	
+  modelValue: { 
+    type: Array,
+    default: null 
+  },
+})
+const emit = defineEmits(['update:modelValue'])
+
+const dispayText = computed(() => {
+  return `经度:${props.modelValue[0] || '请填写'} 纬度:${props.modelValue[1] || '请填写'}`;
+});
+function onPick() {
+  uni.chooseLocation({
+    latitude: props.modelValue[1] as number,
+    longitude: props.modelValue[0] as number,
+    success: (res) => {
+      emit('update:modelValue', [res.longitude, res.latitude]);
+    },
+    fail: (e) => {
+      console.log(e)
+    },
+  });
+}
+</script>

+ 50 - 0
src/common/components/form/components/RichTextEditor.vue

@@ -0,0 +1,50 @@
+<template>
+  <view class="d-flex flex-col">
+    <text v-if="modelValue">已编写内容,总字数 {{ modelValue.length }} 字</text>
+    <text v-else>未编写内容,点击编写</text>
+    <view class="d-flex flex-row align-center gap-s mt-3">
+      <u-button @click="preview">预览内容</u-button>
+      <u-button @click="edit" type="primary">编辑内容</u-button>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import { onPageShow } from '@dcloudio/uni-app';
+
+const props = defineProps({	
+  modelValue: { 
+    type: String,
+    default: null 
+  },
+})
+const emit = defineEmits(['update:modelValue'])
+let editorOpened = false;
+
+function preview() {
+  uni.setStorage({
+    key: 'editorContent',
+    data: props.modelValue,
+    success: () => navTo('/pages/article/editor/preview'),
+  })
+}
+function edit() {
+  editorOpened = true;
+  uni.setStorage({
+    key: 'editorContent',
+    data: props.modelValue,
+    success: () => navTo('/pages/article/editor/editor'),
+  })
+}
+
+onPageShow(() => {
+  if (editorOpened) {
+    editorOpened = false;
+    uni.getStorage({
+      key: 'editorContent',
+      success: (success) => emit('update:modelValue', success.data),
+    })
+  }
+})
+</script>

+ 42 - 0
src/common/components/form/form/Form.vue

@@ -0,0 +1,42 @@
+<template>
+  <view class="nana-form">
+    <slot />
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+import type { FormDefineItem } from '..';
+
+const props = defineProps({	
+  model: {
+    type: Object,
+    default: () => ({})
+  },
+  rules: {
+    type: Object as PropType<FormDefineItem['rules']>,
+    default: () => ({}) 
+  }
+});
+
+const formContext = {
+  addFormItem: (item: {
+    key: string,
+  }) => {
+    console.log('addFormItem', item);
+  }
+}
+
+defineExpose({
+
+})
+
+</script>
+
+<style lang="scss">
+.nana-form {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+</style>

+ 14 - 0
src/common/components/form/form/FormItem.vue

@@ -0,0 +1,14 @@
+<template>
+
+
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss">
+.nana-form-item {
+
+}
+</style>

+ 159 - 0
src/common/components/form/index.ts

@@ -0,0 +1,159 @@
+export interface FormDefine {
+  /**
+   * Todo: page
+   */
+  type?: 'flat'|'page'|'group',
+  props?: any;
+  propNestType?: 'flat'|'nest'|'array',
+  items: FormDefineItem[];
+}
+
+
+/**
+ * 表单动态属性定义
+ */
+export declare type IFormItemCallback<T> = {
+  /**
+   * 预留,暂未使用
+   */
+  type?: string;
+  /**
+   * @param model 当前表单条目的值
+   * @param rawModel 整个 form 的值 (最常用,当两个关联组件距离较远时,可以从顶层的 rawModel 里获取)
+   * @param parentModel 父表单元素的值 (上一级的值,只在列表场景的使用,例如列表某个元素的父级就是整个 item)
+   * @param item 当前表单条目信息
+   */
+  callback: (model: any, rawModel: any, parentModel: any, formGlobalParams: any, item: FormDefineItem) => T;
+};
+export type IFormItemCallbackAdditionalProps<T> = { [P in keyof T]?: T[P]|IFormItemCallback<T[P]> }
+
+export interface FormRulesItem {
+  /**
+   * 是否必填,默认false
+   */
+  required?: boolean;
+  /**
+   * 数组至少要有一个元素,且数组内的每一个元素都是唯一的。
+   */
+  range?: any[];
+  /**
+   * 内置校验规则,如这些规则无法满足需求,可以使用正则匹配或者自定义规则
+   */
+  format?: string;
+  /**
+   * 正则表达式,注意事项见下方说明
+   */
+  pattern?: RegExp;
+  /**
+   * 校验最大值(大于)
+   */
+  maximum?: number;
+  /**
+   * 校验最小值(小于)
+   */
+  minimum?: number;
+  /**
+   * 校验数据最小长度
+   */
+  minLength?: number;
+  /**
+   * 校验数据最大长度
+   */
+  maxLength?: number;
+  /**
+   * 校验失败提示信息语,可添加属性占位符,当前表格内属性都可用作占位符
+   */
+  errorMessage?: string;
+  /**
+   * 自定义校验规则
+   */
+  validateFunction?: (rule: any, value: any, data: any, callback: (e: any) => void) => boolean|undefined|void;
+}
+
+export interface FormDefineItem {
+  /**
+   * 表单项显示标签
+   */
+  label?: string|IFormItemCallback<string>;
+  /**
+   * 属性名称
+   */
+  name: string;
+  fullName?: string;
+  /**
+   * 表单项组件类型
+   */
+  type?: string;
+  /**
+   * 传递给条目组件的参数。(允许动态回调)
+   */
+  params?: Record<string, unknown|IFormItemCallback<unknown>>|unknown;
+  /**
+   * 传递给FormItem组件的参数
+   */
+  itemParams?: any;
+  /**
+   * 默认值,用于默认数据生成
+   */
+  defaultValue?: any;
+  /**
+   * 当前条目的校验规则
+   */
+  rules?: FormRulesItem[],
+  /**
+   * 子条目,在对象中为对象子属性,在数组中为数组条目(单条目按单项控制,多条目按对象看待控制)
+   */
+  children?: FormDefine,
+
+  //todo:联动
+
+  /**
+   * 是否显示。当为undefined时,默认显示。
+   */
+  show?: boolean|IFormItemCallback<boolean>|undefined,
+
+  /**
+   * 当前条目组件加载时发生事件
+   * @param topModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  onMounted?: (topModel: any, ref: any) => void;
+
+  /**
+   * 当前条目组件卸载时发生事件
+   * @param topModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  onBeforeUnMount?: (topModel: any, ref: any) => void;
+
+  /**
+   * 当前条目数据更改时发生事件
+   * @param oldValue 旧值
+   * @param newValue 新值
+   * @param topModel 顶层数据对象
+   * @param ref 组件实例
+   * @returns 
+   */
+  onChange?: (oldValue: any, newValue: any, topModel: any, ref: any) => void;
+}
+export interface FormExport {
+  /**
+   * 初始化表单数据对象
+   */
+  initFormData(data: () => any): void;
+  /**
+   * 加载表单数据
+   * @param value 表单数据
+   */
+  loadFormData(value?: Record<string, any>): void;
+  /**
+   * 提交表单
+   */
+  submitForm<T = Record<string, any>>(): Promise<T|null>;
+  /**
+   * 重置整个表单数据
+   */
+  resetForm(): void;
+}

File diff suppressed because it is too large
+ 363 - 0
src/common/components/sunui-upimg/sunui-upimg.vue


+ 17 - 0
src/common/composeabe/ErrorDisplay.ts

@@ -0,0 +1,17 @@
+export function showError(e: any, title = '糟糕,出错了', callback?: () => void) {
+  console.log('showError', e);
+  let message = '';
+  if (e?.errMsg) 
+    message = e.errMsg;
+  else 
+    message = '' + (e ?? '未知错误');
+  uni.showModal({ 
+    title,
+    content: message,
+    showCancel: false,
+    success() {
+      callback?.();
+    }
+  })
+  uni.hideLoading();
+}

+ 42 - 0
src/common/composeabe/LoadQuerys.ts

@@ -0,0 +1,42 @@
+import { onLoad } from "@dcloudio/uni-app";
+import { nextTick, ref, type Ref } from "vue";
+
+/**
+ * 用于在页面加载时获取并处理页面参数的组合式函数。
+ * @param defaults - 页面参数的默认值对象,其类型为泛型 T,T 需继承自 Record<string, any>。
+ * @param afterLoad - 可选的回调函数,在页面加载完成且参数处理完毕后执行,接收处理后的参数对象作为参数。
+ */
+export function useLoadQuerys<T extends Record<string, any>>(
+  defaults: T, 
+  afterLoad?: (querys: T) => void
+) {
+  const querys = ref<T>(defaults) as Ref<T>; 
+
+  onLoad((_querys) => {
+    if (_querys) {
+      for (const key in querys.value) {
+        if (typeof defaults[key] === 'number')
+          (querys.value as Record<string, any>)[key] = Number(_querys[key]); 
+        else
+          querys.value[key] = _querys[key];
+      }
+    }
+    nextTick(() => {
+      afterLoad?.(querys.value);
+    });
+  });
+
+  return {
+    querys,
+  }
+}
+
+export function stringDotNumbersToNumbers(a: number|number[]|string|undefined): number|number[]|undefined {
+  if (typeof a === 'string') {
+    if (a.includes(','))
+      return a.split(',').map(stringDotNumbersToNumbers) as number[]; 
+    else
+      return Number(a.replace(/\./g, '')); 
+  }
+  return a;
+}

+ 9 - 0
src/common/composeabe/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>;
+}

+ 26 - 0
src/common/composeabe/RequireLogin.ts

@@ -0,0 +1,26 @@
+import { useAuthStore } from "@/store/auth";
+import { confirm } from "@imengyu/imengyu-utils/dist/uniapp/DialogAction";
+import { navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
+
+export function useReqireLogin() {
+  const authStore = useAuthStore();
+ 
+  return {
+    requireLogin(cb: () => void, message: string = '登录后查看') {
+      if (!authStore.isLogged) {
+        confirm({ 
+          title: '提示', 
+          content: message,
+          confirmText: '去登录' 
+        }).then((res) => {
+          if (res) {
+            navTo('/pages/user/login');
+            return;
+          }
+        })
+      } else {
+        cb();
+      }
+    }
+  }
+}

+ 64 - 0
src/common/composeabe/SimpleDataLoader.ts

@@ -0,0 +1,64 @@
+import { onMounted, ref, type Ref } from "vue";
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+
+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,
+  showGlobalLoading = false,
+)  : 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';
+    if (showGlobalLoading)
+      uni.showLoading({ title: '加载中...' });
+
+    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 = '' + e;
+      loadStatus.value = 'error';
+      console.log(e);
+      
+    } finally {
+      if (showGlobalLoading)
+        uni.hideLoading();
+    }
+  }
+
+  onMounted(() => {
+    if (loadWhenMounted) {
+      setTimeout(() => {
+        loadData();
+      }, (0.5 + Math.random()) * 500);
+    }
+  })
+
+  return {
+    content,
+    loadStatus,
+    loadError,
+    loadData,
+    getLastParams: () => lastParams,
+  }
+}

+ 97 - 0
src/common/composeabe/SimpleLocalDataStorage.ts

@@ -0,0 +1,97 @@
+import { onMounted, ref, type Ref } from "vue";
+
+
+export function useSimpleLocalDataStorage<T>(
+  subKey: string, 
+  requireValueAtLoad = false,
+  defaultValue: T|null = null,
+) {
+  const key = `SimpleLocalDataStorage.${subKey}`;
+
+  async function get() : Promise<T | null> {
+    try {
+      const res = await uni.getStorage({ key });
+      if (res.data) 
+        return JSON.parse(res.data) as T;
+    } catch (e) {
+      console.error(e);
+    }
+    return defaultValue ?? null;
+  }
+  async function set(newValue: T|null) {
+    if (newValue === null)
+      await uni.removeStorage({ key });
+    else
+      await uni.setStorage({ key, data: JSON.stringify(newValue) });
+    value.value = newValue;
+  }
+
+  async function update(fn: (oldValue: T|null) => T|null) {
+    const oldValue = await get();
+    const newValue = fn(oldValue);
+    await set(newValue);
+    return newValue;
+  }
+  const value = ref<T|null>(defaultValue) as Ref<T|null>;
+
+  onMounted(async () => {
+    if (requireValueAtLoad)
+      value.value = await get() as T;
+  })
+
+  return {
+    get, 
+    set,
+    update,
+    value,
+  }
+}
+export function useSimpleLocalArrayDataStorage<T>(
+  subKey: string, 
+  requireValueAtLoad = false,
+  defaultValue: T[]|null = null,
+) {
+
+  const {
+    value,
+    get, 
+    set,
+    update,
+  } = useSimpleLocalDataStorage<T[]>(subKey, requireValueAtLoad, defaultValue);
+
+  async function arrayRemove(index: number) {
+    return await update((a) => {
+      if (a instanceof Array)
+        return a.filter((_, i) => i !== index);
+      return a;
+    });
+  }
+  async function arrayPush(newItem: T) {
+    return await update((a) => {
+      if (a instanceof Array) {
+        a.push(newItem);
+        return a;
+      }
+      return a;
+    }); 
+  }
+  async function arrayUpdate(index: number, updateItem: (old: T) => T) {
+    return await update((a) => {
+      if (a instanceof Array) {
+        a[index] = updateItem(a[index]);
+        return a;
+      }
+      return a;
+    }); 
+  }
+
+  return {
+    get, 
+    set,
+    update,
+    arrayRemove,
+    arrayPush,
+    arrayUpdate,
+    value,
+  }
+}

+ 40 - 0
src/common/composeabe/SimplePageContentLoader.ts

@@ -0,0 +1,40 @@
+import { ref, type Ref } from "vue";
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+
+export interface ISimplePageContentLoader<T, P> extends ILoaderCommon<P> {
+  content: Ref<T|null>;
+}
+
+export function useSimplePageContentLoader<T, P = any>(
+  loader: (params?: P) => Promise<T>
+)  : ISimplePageContentLoader<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;
+      loadStatus.value = 'finished';
+      loadError.value = '';
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+    }
+  }
+
+  return {
+    content,
+    loadStatus,
+    loadError,
+    loadData,
+  }
+}

+ 80 - 0
src/common/composeabe/SimplePageListLoader.ts

@@ -0,0 +1,80 @@
+import { onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
+import { ref, type Ref } from "vue";
+import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+
+export interface ISimplePageListLoader<T, P> extends ILoaderCommon<P> {
+  list: Ref<T[]>;
+  page: Ref<number>;
+  total: Ref<number>;
+}
+
+export function useSimplePageListLoader<T, P = any>(
+  pageSize: number, 
+  loader: (page: number, pageSize: number, params?: P) => Promise<{ list: T[], total: number }>,
+  showGlobalLoading = false,
+)  : ISimplePageListLoader<T, P>
+{
+  
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+  const page = ref(0);
+  const total = ref(0);
+  const list = ref<T[]>([]) as Ref<T[]>;
+
+  let lastParams: P | undefined;
+  let loading = false;
+
+  async function loadData(params?: P, refresh: boolean = false) {
+    if (loading) 
+      return;
+    if (params)
+      lastParams = params;
+    if (refresh) {
+      page.value = 0;
+      list.value = []; 
+    }
+    page.value++;
+    loadStatus.value = 'loading';
+    loading = true;
+    if (showGlobalLoading)
+      uni.showLoading({ title: '加载中...' });
+
+    try {
+      const res = (await loader(page.value, pageSize, lastParams));
+      list.value = list.value.concat(res.list as T[]);
+      total.value = res.total;
+      loadStatus.value = res.list.length > 0 ? 'finished' : 'nomore';
+      loadError.value = '';
+      loading = false;
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      loading = false;
+    } finally {
+      if (showGlobalLoading)
+        uni.hideLoading();
+    }
+  }
+
+  onPullDownRefresh(() => {
+    loadData(lastParams, true).then(() => {
+      uni.stopPullDownRefresh();
+    }).catch(() => {
+      uni.stopPullDownRefresh();
+    });
+  });
+  onReachBottom(() => {
+    if (loadStatus.value == 'nomore')
+      return;
+    loadData(lastParams, false);
+  });
+
+  return {
+    list,
+    total,
+    page,
+    loadStatus,
+    loadError,
+    loadData,
+  }
+}

+ 10 - 0
src/common/composeabe/SwiperImagePreview.ts

@@ -0,0 +1,10 @@
+export function useSwiperImagePreview(getList: () => string[]) {
+  return {
+    onPreviewImage: (index: number) => {
+      uni.previewImage({
+        current: index,
+        urls: getList()
+      })
+    }
+  }
+}

+ 35 - 0
src/common/composeabe/TabControl.ts

@@ -0,0 +1,35 @@
+import { computed, ref, watch } from "vue";
+
+export interface TabControlItem {
+  name: string,
+  [key: string]: any,
+}
+
+export function useTabControl(options: {
+  tabs?: TabControlItem[],
+  onTabChange?: (tab: number, tabId: number) => void,
+}) {
+
+  const tabCurrentIndex = ref(0)
+  const tabCurrentId = ref(0)
+  const tabsArray = ref<TabControlItem[]>(options.tabs ?? []);
+
+  watch(tabCurrentIndex, (v) => {
+    options.onTabChange?.(v, tabCurrentId.value) 
+  })
+
+  const tabs = computed(() => {
+    return tabsArray.value.filter(t => t.visible !== false)
+  })
+
+  return {
+    tabCurrentId,
+    tabCurrentIndex,
+    tabs,
+    tabsArray,
+    onTabClick(e: any) {
+      tabCurrentIndex.value = e.index
+      tabCurrentId.value = e.id
+    }
+  }
+}

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

@@ -0,0 +1,9 @@
+
+/**
+ * 说明:后端接口配置
+ */
+export default {
+  serverDev: 'https://mn.wenlvti.net/api',
+  serverProd: 'https://mn.wenlvti.net/api',
+  mainBodyId: 2,
+}

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

@@ -0,0 +1,23 @@
+
+/**
+ * 说明:应用静态配置
+ */
+export default {
+  version: '0.0.1',
+  appId: 'wx57b317f69760fe9e',
+  qqMapKey: 'TOIBZ-CA4WB-OFQUF-J3XG4-EEB2J-DXBX7',
+  amapKey: '34eb1d57f93720a871bd11a90af0c91c',
+  noLoginPages: [
+    '/pages/user/login',
+    '/pages/user/register',
+    '/pages/user/reset-password',
+  ],
+}
+
+/**
+ * 图炫地图配置
+ */
+export function configAiMap() {
+}
+
+export const isDev = process.env.NODE_ENV === 'development';

+ 7 - 0
src/common/config/ImagesUrls.ts

@@ -0,0 +1,7 @@
+
+const root = 'https://mncdn.wenlvti.net/app_static/minnan/';
+
+export default {
+  IconMarker: `${root}/images/icon_marker.png`,
+  defaultImage: 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png',
+}

+ 969 - 0
src/common/scss/common.scss

@@ -0,0 +1,969 @@
+@use "font.scss" as *;
+@use "font_num.scss" as *;
+
+page {
+  background: #F8F8F8;
+  color: #111111;
+}
+
+view {
+  font-size: 14px;
+  line-height: inherit;
+}
+
+.main {
+  padding: 32rpx;
+  &.white{
+    background: #fff;
+  }
+}
+
+.search {
+  margin-bottom: 20rpx;
+
+  ::v-deep .uni-searchbar__box {
+    border: 1px solid #6e6e6e;
+  }
+
+  &.with-button {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 0;
+
+    ::v-deep {
+      .uni-searchbar {
+        width: 500rpx;
+      }
+      .u-button {
+        border-radius: 40rpx;
+      }
+    }
+  }
+}
+.text-center{
+  text-align: center!important;
+}
+.category {
+  display: flex;
+  margin-top: 40rpx;
+  margin-bottom: 38rpx;
+  align-items: flex-end;
+  &.sm{
+    .name{
+      font-size: 32rpx;
+    }
+  }
+  .name {
+    flex: 1;
+    font-size: 36rpx;
+    color: #111111;
+    font-weight: 600;
+    display: flex;
+  }
+
+  .more {
+    font-size: 24rpx;
+    color: #666666;
+  }
+}
+
+.artifact-list {
+  padding-bottom: 50rpx;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+
+  .item {
+    margin-bottom: 36rpx;
+    position: relative;
+    overflow: hidden;
+    width: calc(50% - 15rpx);
+
+    .image-wrap {
+      width: 330rpx;
+      height: 330rpx;
+      background-size: cover;
+      background-position: center;
+      border-radius: 20rpx;
+    }
+
+    .name {
+      font-weight: 800;
+      font-size: 30rpx;
+      color: #333333;
+      margin-top: 20rpx;
+      width: 100%;
+      text-align: center;
+      height: 40rpx;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+.mask {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0) 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .iconfont {
+    font-size: 80rpx;
+    color: #fff;
+    position: absolute;
+  }
+}
+.post-list {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+
+
+  .item {
+    padding: 0;
+    margin-bottom: 38rpx;
+    background: #fff;
+    border-radius: 20rpx 20rpx 0 0;
+    overflow: hidden;
+    width: calc(50% - 10rpx);
+
+    &:active, &.pressed {
+      background: #efefef;
+    }
+
+    .image-wrap {
+      position: relative;
+      width: 330rpx;
+      height: 440rpx;
+      background: #fff center;
+      background-size: cover;
+
+      .like {
+        position: absolute;
+        right: 24rpx;
+        bottom: 20rpx;
+        z-index: 99;
+        color: #fff;
+        font-size: 24rpx;
+        &.liked{
+          color: #FF8719;
+          .iconfont {
+            color: #FF8719;
+          }
+        }
+        .iconfont {
+          font-size: 26rpx;
+          color: #fff;
+          display: inline-block;
+          margin-right: 6rpx;
+        }
+
+      }
+    }
+
+    .desc {
+      margin: 20rpx 16rpx 30rpx;
+      text-align: justify;
+      font-size: 28rpx;
+      color: #666666;
+      line-height: 48rpx;
+    }
+  }
+}
+.banner {
+  margin-top: 10rpx;
+  .swiper {
+    overflow: hidden;
+    height: 246rpx;
+    border-radius: 28rpx;
+    .item {
+      height: 100%;
+      image {
+        height: 100%;
+        width: 100%;
+        border-radius: 28rpx;
+        display: block;
+      }
+    }
+  }
+}
+
+.category-tag{
+  font-size: 24rpx;
+  color:#fff;
+  margin-left: 10rpx;
+  display: flex;
+  align-items: flex-end;
+  text{
+    display: inline-block;
+    background:#FF8719;
+    padding:6rpx 10rpx;
+  }
+  .triangle{
+    padding: 0;
+    background:#FF8719;
+    height:10rpx; // 增加高度
+    width:24rpx; // 调整为正方形
+    clip-path: polygon(0 100%, 100% 0, 100% 100%);
+  }
+}
+/** 图文列表 水平 */
+.complex-list-horizontal-1 {
+  &.lg{
+    .item{
+      image, .image-wrapper,.u-image{
+        width: 262rpx;
+        height: 286rpx;
+        overflow: hidden;
+      }
+      .info{
+        .name{
+          margin-bottom: 10rpx;
+        }
+        .desc{
+          line-height: 45rpx;
+        }
+      }
+    }
+  }
+  .item {
+    padding: 0;
+    margin-bottom: 30rpx;
+    background: #fff;
+    display: flex;
+    position: relative;
+    border-radius: 20rpx;
+    overflow: hidden;
+    image,.image-wrapper,.u-image {
+      display: block;
+      border-radius: 20rpx;
+      width: 170rpx;
+      height: 190rpx;
+      flex-shrink: 0;
+      margin-right: 30rpx;
+      overflow: hidden;
+      background-color: #efefef;
+    }
+
+    .info {
+      padding-right: 30rpx;
+      .name {
+        margin-top: 32rpx;
+        font-size: 30rpx;
+        color: #312520;
+        font-weight: 600;
+        margin-bottom: 16rpx;
+        line-height: 48rpx;
+      }
+      .desc {
+        text-align: justify;
+        font-size: 28rpx;
+        color: #666666;
+        line-height: 48rpx;
+      }
+    }
+  }
+}
+.complex-list-horizontal-2 {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+
+  .item {
+    padding: 0;
+    margin-bottom: 30rpx;
+    background: #fff;
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    border-radius: 20rpx;
+    overflow: hidden;
+    width: calc(50% - 15rpx);
+
+    image,.image-wrapper,.u-image {
+      display: block;
+      border-radius: 20rpx;
+      width: 100%;
+      height: 300rpx;
+      flex-shrink: 0;
+      overflow: hidden;
+      background-color: #efefef;
+    }
+
+    .desc {
+      text-align: justify;
+      font-size: 28rpx;
+      color: #666666;
+      line-height: 48rpx;
+      padding: 10rpx 20rpx; 
+    }
+  }
+}
+.ellipsis-1 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.ellipsis-2 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.ellipsis-4 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.entrance{
+  background: #FFFFFF;
+  padding:24rpx 38rpx;
+  border-radius: 20rpx;
+  margin-bottom: 42rpx;
+  margin-top: 10rpx;
+  display: flex;
+  flex-wrap: nowrap;
+  .item{
+    text-align: center;
+    width: 25%;
+    image{
+      width: 95rpx;
+      height: 95rpx;
+    }
+    .title{
+      font-weight: 600;
+      font-size: 28rpx;
+      color: #111111;
+      margin-top: 12rpx;
+    }
+  }
+
+}
+
+.news-simple-list-with-stats {
+  background: #fff;
+  padding: 32rpx 32rpx 32rpx;
+  margin: 0 -32rpx;
+  .item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 50rpx;
+    height: 130rpx;
+    overflow: hidden;
+    &:last-child{
+      margin-bottom: 0;
+    }
+    image,.image-wrapper,.u-image  {
+      width: 230rpx;
+      height: 130rpx;
+      border-radius: 10rpx;
+      margin-right: 20rpx;
+    }
+
+    .info {
+      flex: 1;
+      .title {
+        font-size: 28rpx;
+        color: #111111;
+        line-height: 42rpx;
+        margin-bottom: 20rpx;
+      }
+      .name {
+        min-height: 76rpx;
+      }
+      .extra {
+        display: flex;
+        font-size: 24rpx;
+        color: #999999;
+        text.iconfont {
+          margin-right: 4rpx;
+          display: inline-block;
+          &.icon-view{
+            margin-left: 20rpx;
+          }
+          &.icon-fav{
+            margin-left: 20rpx;
+          }
+        }
+      }
+    }
+  }
+}
+
+::v-deep .swiper .wx-swiper-dot {
+  width: 16rpx;
+  height: 16rpx;
+  background: rgba(0,0,0,0.5);
+  border: 4rpx solid #fff;
+  border-radius: 50%;
+  transition: all 0.3s ease;
+}
+
+::v-deep .swiper .wx-swiper-dot-active {
+  width: 44rpx;
+  height: 16rpx;
+  background: #FF8719;
+  border-radius: 20rpx;
+  border: 4rpx solid #FFFFFF;
+  opacity: 1;
+}
+::v-deep  .swiper.right-indicator .wx-swiper-dots {
+  left: inherit;
+  right: -20rpx;
+}
+.compound-list.scene-list {
+  .item {
+    flex-direction: column;
+    height: 370rpx;
+    image{
+      display: block;
+      width: 100%;
+      height:282rpx;
+    }
+    .info{
+      display: flex;
+      padding:20rpx;
+      justify-content: center;
+      .name{
+        flex:1;
+        margin-top: 6rpx;
+      }
+      .desc{
+        color:#24515D;
+        font-size: 24rpx;
+        text.iconfont{
+          font-size: 24rpx;
+          color:#999999;
+          display: inline-block;
+          margin-left: 6rpx;
+        }
+      }
+    }
+  }
+}
+/**
+  上图下标题
+ */
+.complex-list-vertical-1 {
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+
+  .item {
+    width: calc(50% - 15rpx);
+    margin-bottom: 53rpx;
+    position: relative;
+    
+    image {
+      width: 100%;
+      height: 360rpx;
+      display: block;
+    }
+
+    .info {
+      text-align: center;
+      padding-top: 20rpx;
+      font-weight: bold;
+      font-size: 30rpx;
+      color: #312520;
+    }
+  }
+}
+.complex-list-vertical-2{
+  .item{
+    margin-bottom: 35rpx;
+    image{
+      width: 687rpx;
+      height: 287rpx;
+      display: block;
+      border-radius: 10rpx;
+    }
+    .info{
+      background: #FFFFFF;
+      display: flex;
+      padding:24rpx 20rpx 29rpx;
+      font-size: 24rpx;
+      color: #24515D;
+      align-items: center;
+      .name{
+        flex:1;
+        font-weight: 600;
+        font-size: 30rpx;
+        color: #111111
+      }
+      text.iconfont{
+        color:#999999;
+      }
+    }
+  }
+}
+/**
+  图文横向滑动
+**/
+.complex-swiper {
+  /* 小一号的尺寸 首页用 */
+  &.sm{
+    .swiper{
+      height: 254rpx;
+
+    }
+    .item{
+      height:100%;
+      width: 425rpx;
+    }
+    .name{
+      right:28rpx;
+      bottom: 22rpx;
+    }
+  }
+  &.lg{
+    .swiper{
+      height: 360rpx;
+
+      .swiper-item {
+        width: 580rpx;
+
+        &.active {
+          z-index: 10;
+
+          .item {
+            transform: scale(1);
+          }
+        }
+      }
+      .item{
+        width: 580rpx;
+        height: 360rpx;
+        transform: scale(0.8);
+        transition: transform 0.3s ease;
+        position: relative;
+      }
+    }
+  }
+  .swiper {
+    height: 300rpx;
+  }
+  .item {
+    width: 514rpx;
+    height: 300rpx;
+    position: relative;
+    border-radius: 20rpx;
+    overflow: hidden;
+    image {
+      width: 100%;
+      height: 100%;
+    }
+    .name {
+      position: absolute;
+      bottom: 14rpx;
+      right: 22rpx;
+      left: 22rpx;
+      color: #fff;
+      font-weight: 600;
+      font-size: 28rpx;
+    }
+  }
+}
+.threeD {
+  width: 36rpx;
+  height: 36rpx;
+  background: rgba(0, 0, 0, 0.57);
+  border-radius: 50%;
+  position: absolute;
+  top: 15rpx;
+  left: 205rpx;
+  z-index: 99;
+  text-align: center;
+
+  text {
+    color: #fff;
+    font-size: 24rpx;
+  }
+}
+page > view{
+  padding-bottom: 120rpx;
+}
+.img-banner {
+  height: 246rpx;
+  width: 100%;
+  border-radius: 20rpx;
+  margin-bottom: 40rpx;
+  overflow: hidden;
+  image {
+    height: 100%;
+    width: 100%;
+  }
+}
+
+.level-info {
+  padding: 48rpx 36rpx 36rpx;
+  background: #FFF2E6;
+  border-radius: 20rpx;
+  position: relative;
+  margin-bottom: 40rpx;
+  > view:first-child {
+    margin-bottom: 44rpx;
+  }
+
+  .label {
+    font-size: 28rpx;
+    color: #111111;
+    display: inline-block;
+  }
+
+  .value {
+    font-weight: bold;
+    font-size: 28rpx;
+    display: inline-block;
+    color: #333333;
+
+    .em {
+      font-family: Rockwell;
+      font-weight: 600;
+      font-size: 30rpx;
+      color: #FF8719;
+      display: inline-block;
+      margin-left: 35rpx;
+    }
+  }
+
+  .btn {
+    position: absolute;
+    top: 30rpx;
+    right: 30rpx;
+    border-radius: 10rpx;
+    border: 1px solid #FF8719;
+    padding: 15rpx 20rpx;
+    display: flex;
+    align-items: center;
+    font-weight: 400;
+    font-size: 28rpx;
+    color: #FF8719;
+
+    text.iconfont {
+      display: inline-block;
+      margin-right: 15rpx;
+      font-size: 40rpx;
+    }
+  }
+}
+.task-list{
+  .item{
+    display: flex;
+    align-items: center;
+    background: #fff;
+    margin-bottom: 36rpx;
+    padding:39rpx 27rpx 38rpx;
+    text.iconfont{
+      width: 91rpx;
+      height: 91rpx;
+      border-radius: 50%;
+      border: 1px solid #25515E;
+      text-align: center;
+      color: #25515E;
+      font-size: 60rpx;
+      line-height: 91rpx;
+      display: inline-block;
+      margin-right: 17rpx;
+    }
+    .btn{
+      background: #FF8719;
+      border-radius: 28rpx;
+      color:#fff;
+      font-size: 28rpx;
+      padding:14rpx 38rpx;
+      &.active{
+        background: #EFEFEF;
+        color:#999999;
+      }
+    }
+    .info{
+      flex:1;
+      .title{
+        font-weight: 600;
+        font-size: 30rpx;
+        color: #333333;
+        margin-bottom: 22rpx;
+      }
+      .desc{
+        font-weight: 400;
+        font-size: 24rpx;
+        color: #999999;
+      }
+    }
+  }
+}
+.people-list{
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  .item{
+    width: 214rpx;
+    background: #fff;
+    text-align: center;
+    position: relative;
+    margin-bottom: 24rpx;
+    padding-top: 80rpx;
+    padding-bottom: 40rpx;
+    border-radius: 10rpx;
+    image.avatar{
+      width: 90rpx;
+      height: 90rpx;
+      border-radius: 50%;
+      margin-bottom: 38rpx;
+    }
+    .name{
+      font-weight: 800;
+      font-size: 30rpx;
+      color: #333333;
+      margin-bottom: 20rpx;
+    }
+    .days{
+      font-weight: 500;
+      font-size: 24rpx;
+      color: #999999;
+    }
+    .rank{
+      position: absolute;
+      top: 35rpx;
+      left: 17rpx;
+      font-size: 48rpx;
+      line-height: 35rpx;
+      color:#B6B6B6;
+      font-style: italic;
+      font-family: Rockwell;
+      &.top{
+        color: #FF8719;
+        font-size: 60rpx;
+      }
+      .num-shadow{
+        width: 45rpx;
+        height: 45rpx;
+        position: absolute;
+        left: 30rpx;
+        top: 0;
+      }
+    }
+  }
+
+}
+button[type="primary"] {
+  background: #FF8719;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  padding: 8rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+.form-title {
+  font-size: 32rpx;
+  color: #333;
+  margin-bottom: 28rpx;
+  margin-top: 30rpx;
+  display: flex;
+  align-items: center;
+  .line {
+    background: #FF8719;
+    border-radius: 3rpx;
+    margin-right: 22rpx;
+    width: 6rpx;
+    height: 34rpx;
+  }
+}
+.form-block {
+  margin-bottom: 32rpx;
+  padding: 24rpx 26rpx;
+  background: #fff;
+  border-radius: 10rpx;
+}
+
+::v-deep .uni-forms-item__label {
+  font-weight: 600;
+  font-size: 28rpx;
+  color: #23262D;
+  line-height: 36rpx;
+}
+
+::v-deep .uni-select {
+  border-radius: 10rpx;
+  //border: 1px solid #ececec;
+  padding: 16rpx 24rpx;
+  font-size: 28rpx;
+  background: #fff;
+}
+
+::v-deep .uni-date-x--border {
+  border-radius: 10rpx;
+  //border: 1px solid #BFBFBF;
+}
+
+::v-deep .uni-input-placeholder, ::v-deep .uni-textarea-placeholder, ::v-deep .uni-select__input-placeholder, ::v-deep .uni-date__x-input, ::v-deep .is-disabled .uni-easyinput__placeholder-class {
+  font-weight: 400;
+  font-size: 28rpx;
+  color: #999999;
+}
+
+::v-deep .uni-easyinput__content.is-disabled {
+  background-color: #fff;
+  cursor: pointer;
+}
+
+.address-select {
+  position: relative;
+  width: 100%; // 添加宽度
+  height: 100%;
+}
+
+.input-wrapper {
+  width: 100%;
+  height: 100%;
+  pointer-events: none; /* 禁用内部元素的点击事件 */
+}
+
+::v-deep .uni-easyinput__content.is-disabled {
+  background-color: #fff;
+  cursor: pointer;
+  width: 100%; // 确保输入框宽度占满
+}
+
+::v-deep .popup-content {
+  text-align: center;
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 22rpx 44rpx 37rpx;
+
+  image {
+    width: 158rpx;
+    height: 186rpx;
+    display: block;
+    margin: 0 auto;
+    margin-bottom: 12rpx;
+  }
+
+  text {
+    font-weight: 500;
+    font-size: 24rpx;
+    color: #666666;
+  }
+}
+.uni-datetime-picker--btn{
+  background:#FF8719!important;
+}
+.bottom-actions {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  width: 100%;
+  height: 75rpx;
+  background: #FFFFFF;
+
+  .action {
+    margin-left: 48rpx;
+    font-weight: 800;
+    font-size: 24rpx;
+    color: #191919;
+    display: flex;
+    align-items: center;
+
+    &:last-child {
+      margin-right: 70rpx;
+    }
+
+    .iconfont {
+      font-size: 30rpx;
+      margin-right: 14rpx;
+    }
+
+    .iconfont.icon-liked {
+      color: #FF8719;
+    }
+  }
+}
+
+.article {
+  .title {
+    font-weight: 800;
+    font-size: 36rpx;
+    color: #1E1E1E;
+    line-height: 60rpx;
+    margin-top: 10rpx;
+  }
+
+  .content {
+    padding-bottom: 100rpx;
+  }
+
+  .info {
+    display: flex;
+    align-items: center;
+    margin-top: 25rpx;
+    margin-bottom: 45rpx;
+
+    .author {
+      font-size: 24rpx;
+      color: #FF8719;
+      margin-right: 16rpx;
+    }
+
+    .time {
+      font-size: 24rpx;
+      color: #999999;
+    }
+  }
+}
+.main-img{
+  width: 100%;
+  height: 394rpx;
+  display: block;
+  image{
+    height: 100%;
+    width: 100%;
+  }
+}
+.article {
+  .title {
+    font-weight: 800;
+    font-size: 36rpx;
+    color: #1E1E1E;
+    line-height: 60rpx;
+    margin-top: 10rpx;
+  }
+
+  .content {
+    padding-bottom: 100rpx;
+  }
+
+  .info {
+    display: flex;
+    align-items: center;
+    margin-top: 25rpx;
+    margin-bottom: 45rpx;
+
+    .author {
+      font-size: 24rpx;
+      color: #FF8719;
+      margin-right: 16rpx;
+    }
+
+    .time {
+      font-size: 24rpx;
+      color: #999999;
+    }
+  }
+}

+ 15 - 0
src/common/scss/define/border-radius.scss

@@ -0,0 +1,15 @@
+$border-radius: (
+  xxxs: 1rpx,
+  xxs: 2rpx,
+  xs: 5rpx,
+  sss: 8rpx,
+  ss: 10rpx,
+  s: 15rpx,
+  base: 20rpx,
+  l: 24rpx,
+  ll: 60rpx,
+  lll: 100rpx,
+  xl: 150rpx,
+  xxl: 200rpx,
+  xxxl: 300rpx,
+);

+ 43 - 0
src/common/scss/define/colors.scss

@@ -0,0 +1,43 @@
+$colors: (
+  primary: #da7f08,
+  text: #333232,
+  text-content: #3d2c08,
+  text-content-second: #817b67, 
+  primary-text: #D9492E,
+  primary-second-text: rgba(217, 73, 46, 0.6),
+  title-text: #432A04,
+  second-text: #e6927e,
+  light-blue: #0059ff,
+  light-primary: #ff871935,
+  light-light-primary: #ff871915,
+  success: #09ad32,
+  warning: #FFD666,
+  error: #ec4545,
+  danger: #e71212,
+  red: #d40f0f,
+	yellow: #ffdf5e,
+	green: #09c338,
+  blue: #0059ff,
+  orange: #f99404,
+  purple: #b810da,
+  dark-purple: #7117a5,
+  brown: #5f1a00,
+  black: #000,
+  white: #fff,
+  base: #f6f2e7,
+  custom: #4A5061,
+  link: #0273F1,
+  light: #fbfaf5,
+  dark: #F1F1F1,
+  muted: #858585,
+  second: #666464,
+  third: #999696,
+  forth: #CCC8C8,
+  place: #ece7da,
+  pure: #fff,
+  mask-white: rgba(255, 255, 255, 0.6),
+  mask-black: rgba(0, 0, 0, 0.6),
+  disabled: #CCC8C8,
+  none: transparent,
+  transparent: transparent,
+);

+ 32 - 0
src/common/scss/define/margin-padding.scss

@@ -0,0 +1,32 @@
+
+$paddings: (
+  none: 0,
+  xxs: 5rpx,
+  xs: 10rpx,
+  sss: 10rpx,
+  ss: 20rpx,
+  s: 30rpx,
+  base: 40rpx,
+  l: 50rpx,
+  ll: 60rpx,
+  lll: 80rpx,
+  xl: 100rpx,
+  xxl: 200rpx,
+  xxxl: 300rpx
+);
+
+$margins: (
+  none: 0,
+  xxs: 5rpx,
+  xs: 10rpx,
+  sss: 10rpx,
+  ss: 20rpx,
+  s: 30rpx,
+  base: 40rpx,
+  l: 50rpx,
+  ll: 60rpx,
+  lll: 80rpx,
+  xl: 100rpx,
+  xxl: 200rpx,
+  xxxl: 300rpx
+);

+ 61 - 0
src/common/scss/define/size.scss

@@ -0,0 +1,61 @@
+
+$font-sizes: (
+  xxxs: 12rpx,
+  xxs: 14rpx,
+  xs: 16rpx,
+  sss: 18rpx,
+  ss: 24rpx,
+  s: 28rpx,
+  base: 30rpx,
+  l: 32rpx,
+  ll: 36rpx,
+  lll: 50rpx,
+  xl: 75rpx,
+  xxl: 100rpx,
+  xxxl: 150rpx,
+);
+
+$image-sizes: (
+  xxs: 14rpx,
+  xs: 16rpx,
+  sss: 18rpx,
+  ss: 24rpx,
+  s: 28rpx,
+  base: 30rpx,
+  l: 35rpx,
+  ll: 40rpx,
+  lll: 50rpx,
+  xl: 75rpx,
+  xxl: 100rpx,
+  xxxl: 150rpx
+);
+
+
+$exact-sizes: (
+	5: 5rpx,
+	10: 10rpx,
+	15: 15rpx,
+	20: 20rpx,
+	25: 25rpx,
+	30: 30rpx,
+	35: 35rpx,
+	40: 40rpx,
+	45: 45rpx,
+	50: 50rpx,
+	60: 60rpx,
+	80: 80rpx,
+	100: 100rpx,
+	150: 150rpx,
+	200: 200rpx,
+	250: 250rpx,
+	300: 300rpx,
+	350: 350rpx,
+	400: 400rpx,
+	450: 450rpx,
+	500: 500rpx,
+	550: 550rpx,
+	600: 600rpx,
+	650: 650rpx,
+	700: 700rpx,
+	750: 750rpx,
+);

+ 42 - 0
src/common/scss/define/wing-height.scss

@@ -0,0 +1,42 @@
+$wings: (
+  none: 0,
+  sss: 6rpx,
+  ss: 12rpx,
+  s: 24rpx,
+  base: 32rpx,
+  l: 36rpx,
+  ll: 40rpx,
+  lll: 80rpx,
+  xl: 100rpx,
+  xxl: 200rpx,
+  xxxl: 300rpx
+);
+
+$heights: (
+  none: 0,
+  xxs: 5rpx,
+  xs: 10rpx,
+  sss: 20rpx,
+  ss: 40rpx,
+  s: 60rpx,
+  base: 80rpx,
+  l: 100rpx,
+  ll: 120rpx,
+  lll: 140rpx,
+  xl: 180rpx,
+  xxl: 240rpx,
+  xxxl: 300rpx
+);
+
+$space: (
+  none: 0,
+  ss: 6rpx,
+  s: 12rpx,
+  base: 16rpx,
+  l: 24rpx,
+  ll: 32rpx,
+  lll: 50rpx,
+  xl: 100rpx,
+  xxl: 200rpx,
+  xxxl: 300rpx
+);

File diff suppressed because it is too large
+ 353 - 0
src/common/scss/font.scss


File diff suppressed because it is too large
+ 4 - 0
src/common/scss/font_num.scss


+ 94 - 0
src/common/scss/global/base.scss

@@ -0,0 +1,94 @@
+//基础公共样式
+
+@use './border.scss' as *;
+@use './color.scss' as *;
+@use './flex.scss' as *;
+@use './radius.scss' as *;
+@use './size.scss' as *;
+@use './text.scss' as *;
+@use './shadow.scss' as *;
+@use './wing-space-height.scss' as *;
+@use './margin-padding.scss' as *;
+@use './grid.scss' as *;
+
+.position {
+	&-relative {
+		position: relative;
+	}
+	&-absolute {
+		position: absolute;
+	}
+	&-fixed {
+		position: fixed;
+	}
+	&-sticky {
+		position: sticky;
+	}
+}
+.d {
+	&-flex {
+		display: flex !important;
+	}
+	/* #ifndef APP-VUE */
+	&-none {
+		display: none !important;
+	}
+	&-inline {
+		display: inline !important;
+	}
+	&-inline-block {
+		display: inline-block !important;
+	}
+	&-block {
+		display: block !important;
+	}
+	&-table {
+		display: table !important;
+	}
+	&-table-row {
+		display: table-row !important;
+	}
+	&-table-cell {
+		display: table-cell !important;
+	}
+	&-inline-flex {
+		display: inline-flex !important;
+	}
+	/* #endif */
+}
+
+.background {
+	/* #ifndef APP-NVUE */
+	z-index: 0;
+	/* #endif */
+	
+	&-deep {
+		/* #ifndef APP-NVUE */
+		z-index: -1;
+		/* #endif */
+	}
+}
+
+.overflow {
+	&-hidden {
+		overflow: hidden;
+	}
+	&-scroll {
+		overflow: scroll;
+	}
+	&-auto {
+		overflow: auto;
+	}
+	&-visible {
+		overflow: visible;
+	}
+}
+
+.visible {
+	&-hidden {
+		visibility: hidden;
+	}
+	&-visible {
+		visibility: visible;
+	}
+}

+ 55 - 0
src/common/scss/global/border.scss

@@ -0,0 +1,55 @@
+//边框公共样式
+@use "../define/colors.scss" as *;
+
+$border-width: 1px;
+
+.border {
+	&-all {
+		@each $key, $color in $colors {
+			&-#{''+$key} {
+				border-color: $color;
+				border-width: $border-width;
+				border-style: solid;
+			}
+		}
+	}
+	&-top {
+		@each $key, $color in $colors {
+			&-#{''+$key} {
+				border-top-color: $color;
+				border-top-width: $border-width;
+				border-top-style: solid;
+			}
+		}
+	}
+	&-bottom {
+		@each $key, $color in $colors {
+			&-#{''+$key} {
+				border-bottom-color: $color;
+				border-bottom-width: $border-width;
+				border-bottom-style: solid;
+			}
+		}
+	}
+	&-left {
+		@each $key, $color in $colors {
+			&-#{''+$key} {
+				border-left-color: $color;
+				border-left-width: $border-width;
+				border-left-style: solid;
+			}
+		}
+	}
+	&-right {
+		@each $key, $color in $colors {
+			&-#{''+$key} {
+				border-right-color: $color;
+				border-right-width: $border-width;
+				border-right-style: solid;
+			}
+		}
+	}
+	&-none {
+		border-width: 0;
+	}
+}

+ 17 - 0
src/common/scss/global/color.scss

@@ -0,0 +1,17 @@
+//颜色相关样式
+@use "../define/colors.scss" as *;
+
+.bg {
+	@each $key, $color in $colors {
+		&-#{''+$key} {
+			background-color: $color;
+		}
+	}
+}
+.color {
+	@each $key, $color in $colors {
+		&-#{''+$key} {
+			color: $color;
+		}
+	}
+}

+ 138 - 0
src/common/scss/global/flex.scss

@@ -0,0 +1,138 @@
+//弹性布局相关样式
+
+.flex {
+	&-row {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row!important;
+	}
+	&-col {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: column!important;
+	}
+	&-column {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: column!important;
+	}
+	&-one {
+		flex: 1;
+	}
+	&-center {
+		justify-content: center;
+		align-items: center;
+	}
+  &-grow-0 {
+    flex-grow: 0 !important;
+  }
+  &-grow-1 {
+    flex-grow: 1 !important;
+  }
+  &-shrink-0 {
+    flex-shrink: 0 !important;
+  }
+  &-shrink-1 {
+    flex-shrink: 1 !important;
+  }
+  &-wrap {
+    flex-wrap: wrap !important;
+
+    &-reverse {
+      flex-wrap: wrap-reverse !important;
+    }
+  }
+  &-nowrap {
+    flex-wrap: nowrap !important;
+  }
+}
+.justify {
+	&-start {
+		justify-content: flex-start;
+	}
+	&-center {
+		justify-content: center;
+	}
+	&-end {
+		justify-content: flex-end;
+	}
+	&-between {
+		justify-content: space-between;
+	}
+	&-around {
+		justify-content: space-around;
+	}
+	&-stretch {
+		justify-content: stretch;
+	}
+}
+.align {
+	&-start {
+		align-items: flex-start;
+	}
+	&-center {
+		align-items: center;
+	}
+	&-end {
+		align-items: flex-end;
+	}
+  &-baseline {
+    align-items: baseline;
+  }
+  &-stretch {
+    align-items: stretch;
+  }
+}
+.full {
+	&-width {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		width: 750rpx;
+		/* #endif */
+	}
+	&-height {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		height: 100%;
+		/* #endif */
+	}
+	&-page-width {
+		width: 750rpx;
+		/* #ifndef APP-NVUE */
+		width: 750rpx;
+		/* #endif */
+	}
+	&-page-height {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		height: 100vh;
+		/* #endif */
+	}
+	&-abs {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+	}
+}
+.gap-0 {
+  &-row-0 {
+    row-gap: 0;
+  }
+  &-column-0 {
+    column-gap: 0;
+  }
+  &-0 {
+    gap: 0;
+  }
+}
+

+ 3 - 0
src/common/scss/global/grid.scss

@@ -0,0 +1,3 @@
+.d-grid {
+  display: grid;
+} 

+ 347 - 0
src/common/scss/global/margin-padding.scss

@@ -0,0 +1,347 @@
+@use '../define/margin-padding.scss' as *;
+
+
+$common-level-sizes: (
+  0: 0,
+  1: 0.25rem,
+  2: 0.5rem,
+  25: 0.75rem,
+  3: 1rem,
+  35: 1.25rem,
+  4: 1.5rem,
+  45: 1.75rem,
+  5: 3rem,
+);
+
+.m {
+  &-auto {
+    margin: auto!important;
+  }
+	@each $key, $size in $common-level-sizes {
+    &-#{$key} {
+      margin: $size;
+    }
+  }
+
+  &l {
+    &-auto {
+      margin-left: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-left: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-left: -$size!important;
+      }
+    }
+  }
+  &r {
+    &-auto {
+      margin-right: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-right: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-right: -$size!important;
+      }
+    }
+  }
+  &t {
+    &-auto {
+      margin-top: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-top: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-top: -$size!important;
+      }
+    }
+  }
+  &b {
+    &-auto {
+      margin-bottom: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-bottom: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-bottom: -$size!important;
+      }
+    }
+  }
+  &x {
+    &-auto {
+      margin-left: auto!important;
+      margin-right: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-left: $size!important;
+        margin-right: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-left: -$size!important;
+        margin-right: -$size!important;
+      }
+    }
+  }
+  &y {
+    &-auto {
+      margin-top: auto!important;
+      margin-bottom: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        margin-top: $size!important;
+        margin-bottom: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        margin-top: -$size!important;
+        margin-bottom: -$size!important;
+      }
+    }
+  }
+}
+.p {
+  &-auto {
+    padding: auto!important;
+  }
+	@each $key, $size in $common-level-sizes {
+    &-#{$key} {
+      padding: $size;
+    }
+  }
+
+  &l {
+    &-auto {
+      padding-left: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-left: -$size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-left: -$size!important;
+      }
+    }
+  }
+  &r {
+    &-auto {
+      padding-right: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        padding-right: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-right: -$size!important;
+      }
+    }
+  }
+  &t {
+    &-auto {
+      padding-top: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        padding-top: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-top: -$size!important;
+      }
+    }
+  }
+  &b {
+    &-auto {
+      padding-bottom: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        padding-bottom: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-bottom: -$size!important;
+      }
+    }
+  }
+  &x {
+    &-auto {
+      padding-left: auto!important;
+      padding-right: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        padding-left: $size!important;
+        padding-right: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-left: -$size!important;
+        padding-right: -$size!important;
+      }
+    }
+  }
+  &y {
+    &-auto {
+      padding-top: auto!important;
+      padding-bottom: auto!important;
+    }
+    @each $key, $size in $common-level-sizes {
+      &-#{$key} {
+        padding-top: $size!important;
+        padding-bottom: $size!important;
+      }
+    }
+    @each $key, $size in $common-level-sizes {
+      &-n#{$key} {
+        padding-top: -$size!important;
+        padding-bottom: -$size!important;
+      }
+    }
+  }
+}
+
+.h-100vh {
+  height: 100vh;
+}
+.h-0 {
+  height: 0;
+}
+.l-0 {
+  left: 0;
+}
+.r-0 {
+  right: 0;
+}
+.t-0 {
+  top: 0;
+}
+.b-0 {
+  bottom: 0;
+}
+
+.margin {
+	&-all {	
+		@each $key, $size in $margins {
+			&-#{$key} {
+				margin: $size;
+			}
+		}
+	}
+	&-top {	
+		@each $key, $size in $margins {
+			&-#{$key} {
+				margin-top: $size;
+			}
+		}
+	}
+	&-bottom {
+		@each $key, $size in $margins {
+			&-#{$key} {
+				margin-bottom: $size;
+			}
+		}
+	}
+	&-left {
+		@each $key, $size in $margins {
+			&-#{$key} {
+				margin-left: $size;
+			}
+		}
+	}
+	&-right {
+		@each $key, $size in $margins {
+			&-#{$key} {
+				margin-right: $size;
+			}
+		}
+	}
+	&-none {
+		margin: 0;
+
+		&-left-right {
+			margin-left: 0;
+			margin-right: 0;
+		}
+		&-top-bottom {
+			margin-top: 0;
+			margin-bottom: 0;
+		}
+	}
+}
+.padding {
+	&-all {	
+		@each $key, $size in $paddings {
+			&-#{$key} {
+				padding: $size;
+			}
+		}
+	}
+	&-top {
+		@each $key, $size in $paddings {
+			&-#{$key} {
+				padding-top: $size;
+			}
+		}
+	}
+	&-bottom {
+		@each $key, $size in $paddings {
+			&-#{$key} {
+				padding-bottom: $size;
+			}
+		}
+	}
+	&-left {
+		@each $key, $size in $paddings {
+			&-#{$key} {
+				padding-left: $size;
+			}
+		}
+	}
+	&-right {
+		@each $key, $size in $paddings {
+			&-#{$key} {
+				padding-right: $size;
+			}
+		}
+	}
+	&-none {
+		padding: 0;
+
+		&-left-right {
+			padding-left: 0;
+			padding-right: 0;
+		}
+		&-top-bottom {
+			padding-top: 0;
+			padding-bottom: 0;
+		}
+	}
+}

+ 51 - 0
src/common/scss/global/radius.scss

@@ -0,0 +1,51 @@
+//圆角相关样式
+@use "../define/border-radius.scss" as *;
+
+.radius {
+  &-round {
+    border-radius: 50%;
+  }
+	&-none {
+		border-radius: 0;
+
+		&-bottom {
+			border-bottom-left-radius: 0;
+			border-bottom-right-radius: 0;
+		}
+		&-top {
+			border-top-left-radius: 0;
+			border-top-right-radius: 0;
+		}
+		&-left {
+			border-bottom-left-radius: 0;
+			border-top-left-radius: 0;
+		}
+		&-right {
+			border-bottom-right-radius: 0;
+			border-top-right-radius: 0;
+		}
+		
+	}
+	@each $key, $size in $border-radius {
+		&-#{''+$key} {
+			border-radius: $size;
+
+			&-bottom {
+				border-bottom-left-radius: $size;
+				border-bottom-right-radius: $size;
+			}
+			&-top {
+				border-top-left-radius: $size;
+				border-top-right-radius: $size;
+			}
+			&-left {
+				border-bottom-left-radius: $size;
+				border-top-left-radius: $size;
+			}
+			&-right {
+				border-bottom-right-radius: $size;
+				border-top-right-radius: $size;
+			}
+		}
+	}
+}

+ 25 - 0
src/common/scss/global/shadow.scss

@@ -0,0 +1,25 @@
+
+
+.shadow-sm {
+  box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.055) !important;
+}
+
+.shadow-s {
+  box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.055) !important;
+}
+
+.shadow {
+  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.075) !important;
+}
+
+.shadow-l {
+  box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.100) !important;
+}
+
+.shadow-lg {
+  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.150) !important;
+}
+
+.shadow-none {
+  box-shadow: none !important;
+}

+ 80 - 0
src/common/scss/global/size.scss

@@ -0,0 +1,80 @@
+//宽高, 大小相关样式
+@use "sass:math";
+@use "sass:list";
+
+@use "../define/size.scss" as *;
+
+$full-width: 750rpx;
+
+.height {
+	//数字形式 : height-100 表示100rpx; height-5 表示5rpx,等等
+	@each $key, $w in $exact-sizes {
+		&-#{''+$key} {
+			height: $w;
+		}
+	}
+}
+.width {
+	&-auto {
+		width: auto;
+	}
+	&-full {
+		width: $full-width;
+	}
+	&-half {
+		width: $full-width*0.5;
+	}
+	&-one-third {
+		width: math.div($full-width, 3);
+	}
+	&-one-fourth {
+		width: $full-width*0.25;
+	}
+	&-one-fifth {
+		width: $full-width*0.2;
+	}
+	&-one-sixth {
+		width: math.div($full-width, 6);
+	}
+	&-one-eighth {
+		width: $full-width*0.125;
+	}
+	&-one-tenth {
+		width: $full-width*0.1;
+	}
+	
+	//数字形式: width-1-2 表示 二分之一; width-4-9 表示 9分之4,等等
+	@for $i from 2 to 10 {
+		&-1-#{$i} {
+			width: math.percentage(math.div(1, $i));
+		}
+		
+		@for $j from 2 to $i {
+			&-#{$j}-#{$i} {
+				width: $full-width*math.div($j, $i);
+			}
+		}
+	}
+	
+	//数字形式 2: width-100 表示100rpx; width-5 表示5rpx,等等
+	@each $key, $w in $exact-sizes {
+		&-#{''+$key} {
+			width: $w;
+		}
+	}
+}
+.size {
+	@each $key, $size in $font-sizes {
+		&-#{''+$key} {
+			font-size: $size;
+		}
+	}
+}
+.image-size {
+	@each $key, $size in $image-sizes {
+		&-#{''+$key} {
+			width: $size;
+			height: $size;
+		}
+	}
+}

+ 169 - 0
src/common/scss/global/text.scss

@@ -0,0 +1,169 @@
+//文字相关样式
+
+.text-shadow {
+	/* #ifndef APP-NVUE */
+	text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
+	
+	&-deep {
+		text-shadow: 1px 1px 5px rgba(0,0,0,0.5);
+	}
+	/* #endif */
+}
+.text-indent {
+	/* #ifndef APP-NVUE */
+	text-indent: 2em;
+	
+	&-1 {
+		text-indent: 1em;
+	}	
+	&-2 {
+		text-indent: 2em;
+	}
+	&-3 {
+		text-indent: 3em;
+	}
+	&-3 {
+		text-indent: 3em;
+	}
+	&-none {
+		text-indent: 0;
+	}
+	/* #endif */
+}
+.text-overflow {
+	&-ellipsis {
+		text-overflow: ellipsis;
+	}
+}
+.text-italic {
+	font-style: italic;
+}
+.text-bold {
+	font-weight: 700 !important;
+}
+.text-bolder {
+	font-weight: bolder !important;
+}
+.text-light {
+	font-weight: 300 !important;
+}
+.text-lowercase {
+  text-transform: lowercase !important;
+}
+
+.text-uppercase {
+  text-transform: uppercase !important;
+}
+
+.text-capitalize {
+  text-transform: capitalize !important;
+}
+
+.text-left {
+  text-align: left !important;
+}
+
+.text-right {
+  text-align: right !important;
+}
+
+.text-center {
+  text-align: center !important;
+}
+.text-lines {
+	&-1 {
+		lines: 1;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 1;
+    line-clamp: 1;
+		/* #endif */
+	}
+	&-2 {
+		overflow: hidden;
+		/* #ifdef APP-NVUE */
+		lines: 2;
+		text-overflow: ellipsis;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 2;
+    line-clamp: 2;
+		/* #endif */
+	}
+	&-3 {
+		overflow: hidden;
+		/* #ifdef APP-NVUE */
+		lines: 3;
+		text-overflow: ellipsis;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 3;
+    line-clamp: 3;
+		/* #endif */
+	}
+	&-4 {
+		overflow: hidden;
+		/* #ifdef APP-NVUE */
+		lines: 4;
+		text-overflow: ellipsis;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 4;
+		/* #endif */
+	}
+	&-5 {
+		overflow: hidden;
+		/* #ifdef APP-NVUE */
+		lines: 5;
+		text-overflow: ellipsis;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 5;
+		/* #endif */
+	}
+	&-6 {
+		overflow: hidden;
+		/* #ifdef APP-NVUE */
+		lines: 6;
+		text-overflow: ellipsis;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: -webkit-box;
+		-webkit-box-orient: vertical;
+		-webkit-line-clamp: 6;
+		/* #endif */
+	}
+}
+.text {
+	&-none-decoration {
+		text-decoration: none;	
+	}
+	&-underline {
+		text-decoration: underline;	
+	}
+	&-line-through {
+		text-decoration: line-through;
+	}
+}
+.text-align {
+	&-left {
+		text-align: left;	
+	}
+	&-center {
+		text-align: center;	
+	}
+	&-right {
+		text-align: right;
+	}
+}

+ 71 - 0
src/common/scss/global/wing-space-height.scss

@@ -0,0 +1,71 @@
+//高度,两翼,空格相关样式
+@use "../define/wing-height.scss" as *;
+
+.height {
+	@each $key, $size in $heights {
+		&-#{''+$key} {
+			height: $size;
+		}
+	}
+}
+.gap {
+	@each $key, $size in $heights {
+		&-#{''+$key} {
+			gap: $size;
+		}
+	}
+}
+.row-gap {
+	@each $key, $size in $heights {
+		&-#{''+$key} {
+			row-gap: $size;
+		}
+	}
+}
+.column-gap {
+	@each $key, $size in $heights {
+		&-#{''+$key} {
+			column-gap: $size;
+		}
+	}
+}
+
+.wing {
+	@each $key, $size in $wings {
+		&-#{''+$key} {	
+			margin-left: $size;
+			margin-right: $size;
+		}
+	}
+}
+.padding-wing {
+	@each $key, $size in $wings {
+		&-#{''+$key} {	
+			padding-left: $size;
+			padding-right: $size;
+		}
+	}
+}
+.space {
+	@each $key, $size in $space {
+		&-#{''+$key} {	
+			margin-top: $size;
+			margin-bottom: $size;
+		}
+	}
+}
+
+.h {
+  @for $i from 0 through 20 {
+    &-#{$i * 5} { 
+      height: $i * 5%;
+    }
+  }
+}
+.w {
+  @for $i from 0 through 20 {
+    &-#{$i * 5} { 
+      width: $i * 5%;
+    }
+  }
+}

+ 8 - 0
src/common/style/commonParserStyle.ts

@@ -0,0 +1,8 @@
+export default {
+  p: 'line-height:1.76;font-size:30rpx;margin-bottom:37rpx;text-indent:2em;color:#111111;text-align:justify;',
+  div: 'line-height:1.76;font-size:30rpx;margin-bottom:37rpx;text-indent:2em;color:#111111;text-align:justify;',
+  img: 'display:block;max-width:100%;height:auto;margin-bottom:30rpx;',
+  h2:'margin-bottom:30rpx;font-size:32rpx;line-height:1.7;',
+  h1:'margin-bottom:30rpx;font-size:36rpx;line-height:1.7;',
+  h3:'margin-bottom:30rpx;font-size:30rpx;line-height:1.7;',
+} as Record<string, string>;

+ 122 - 0
src/common/utils/ConvertRgeistry.ts

@@ -0,0 +1,122 @@
+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') {
+        if (source === '') 
+          return { success: true, result: [] }
+        return {
+          success: true,
+          result: source?.split(',') || [],
+        }
+      }
+      return {
+        success: false,
+        convertFailMessage: `[${key}] 不是字符串类型`,
+      };
+    }
+  })
+  DataConverter.registerConverter({
+    key: 'ArrayNumber',
+    targetType: 'arrayInt',
+    converter: (source, key, type) => {
+      if (source instanceof Array) {
+        const result = source.map((p) => {
+          if (typeof p === 'number') return p;
+          return parseInt(p);
+        });
+        return {
+          success: true,
+          result,
+        }
+      }
+      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/env.d.ts

@@ -0,0 +1,8 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import { DefineComponent } from 'vue'
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 561 - 0
src/libs/amap-wx.130.js

@@ -0,0 +1,561 @@
+function AMapWX(a) {
+  this.key = a.key;
+  this.requestConfig = {
+    key: a.key,
+    s: "rsx",
+    platform: "WXJS",
+    appname: a.key,
+    sdkversion: "1.2.0",
+    logversion: "2.0",
+  };
+  this.MeRequestConfig = {
+    key: a.key,
+    serviceName: "https://restapi.amap.com/rest/me",
+  };
+}
+AMapWX.prototype.getWxLocation = function (a, b) {
+  wx.getLocation({
+    type: "gcj02",
+    success: function (c) {
+      c = c.longitude + "," + c.latitude;
+      wx.setStorage({ key: "userLocation", data: c });
+      b(c);
+    },
+    fail: function (c) {
+      wx.getStorage({
+        key: "userLocation",
+        success: function (d) {
+          d.data && b(d.data);
+        },
+      });
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getMEKeywordsSearch = function (a) {
+  if (!a.options)
+    return a.fail({
+      errCode: "0",
+      errMsg: "\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570",
+    });
+  var b = a.options,
+    c = this.MeRequestConfig,
+    d = {
+      key: c.key,
+      s: "rsx",
+      platform: "WXJS",
+      appname: a.key,
+      sdkversion: "1.2.0",
+      logversion: "2.0",
+    };
+  b.layerId && (d.layerId = b.layerId);
+  b.keywords && (d.keywords = b.keywords);
+  b.city && (d.city = b.city);
+  b.filter && (d.filter = b.filter);
+  b.sortrule && (d.sortrule = b.sortrule);
+  b.pageNum && (d.pageNum = b.pageNum);
+  b.pageSize && (d.pageSize = b.pageSize);
+  b.sig && (d.sig = b.sig);
+  wx.request({
+    url: c.serviceName + "/cpoint/datasearch/local",
+    data: d,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (e) {
+      (e = e.data) && e.status && "1" === e.status && 0 === e.code
+        ? a.success(e.data)
+        : a.fail({ errCode: "0", errMsg: e });
+    },
+    fail: function (e) {
+      a.fail({ errCode: "0", errMsg: e.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getMEIdSearch = function (a) {
+  if (!a.options)
+    return a.fail({
+      errCode: "0",
+      errMsg: "\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570",
+    });
+  var b = a.options,
+    c = this.MeRequestConfig,
+    d = {
+      key: c.key,
+      s: "rsx",
+      platform: "WXJS",
+      appname: a.key,
+      sdkversion: "1.2.0",
+      logversion: "2.0",
+    };
+  b.layerId && (d.layerId = b.layerId);
+  b.id && (d.id = b.id);
+  b.sig && (d.sig = b.sig);
+  wx.request({
+    url: c.serviceName + "/cpoint/datasearch/id",
+    data: d,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (e) {
+      (e = e.data) && e.status && "1" === e.status && 0 === e.code
+        ? a.success(e.data)
+        : a.fail({ errCode: "0", errMsg: e });
+    },
+    fail: function (e) {
+      a.fail({ errCode: "0", errMsg: e.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getMEPolygonSearch = function (a) {
+  if (!a.options)
+    return a.fail({
+      errCode: "0",
+      errMsg: "\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570",
+    });
+  var b = a.options,
+    c = this.MeRequestConfig,
+    d = {
+      key: c.key,
+      s: "rsx",
+      platform: "WXJS",
+      appname: a.key,
+      sdkversion: "1.2.0",
+      logversion: "2.0",
+    };
+  b.layerId && (d.layerId = b.layerId);
+  b.keywords && (d.keywords = b.keywords);
+  b.polygon && (d.polygon = b.polygon);
+  b.filter && (d.filter = b.filter);
+  b.sortrule && (d.sortrule = b.sortrule);
+  b.pageNum && (d.pageNum = b.pageNum);
+  b.pageSize && (d.pageSize = b.pageSize);
+  b.sig && (d.sig = b.sig);
+  wx.request({
+    url: c.serviceName + "/cpoint/datasearch/polygon",
+    data: d,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (e) {
+      (e = e.data) && e.status && "1" === e.status && 0 === e.code
+        ? a.success(e.data)
+        : a.fail({ errCode: "0", errMsg: e });
+    },
+    fail: function (e) {
+      a.fail({ errCode: "0", errMsg: e.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getMEaroundSearch = function (a) {
+  if (!a.options)
+    return a.fail({
+      errCode: "0",
+      errMsg: "\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570",
+    });
+  var b = a.options,
+    c = this.MeRequestConfig,
+    d = {
+      key: c.key,
+      s: "rsx",
+      platform: "WXJS",
+      appname: a.key,
+      sdkversion: "1.2.0",
+      logversion: "2.0",
+    };
+  b.layerId && (d.layerId = b.layerId);
+  b.keywords && (d.keywords = b.keywords);
+  b.center && (d.center = b.center);
+  b.radius && (d.radius = b.radius);
+  b.filter && (d.filter = b.filter);
+  b.sortrule && (d.sortrule = b.sortrule);
+  b.pageNum && (d.pageNum = b.pageNum);
+  b.pageSize && (d.pageSize = b.pageSize);
+  b.sig && (d.sig = b.sig);
+  wx.request({
+    url: c.serviceName + "/cpoint/datasearch/around",
+    data: d,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (e) {
+      (e = e.data) && e.status && "1" === e.status && 0 === e.code
+        ? a.success(e.data)
+        : a.fail({ errCode: "0", errMsg: e });
+    },
+    fail: function (e) {
+      a.fail({ errCode: "0", errMsg: e.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getGeo = function (a) {
+  var b = this.requestConfig,
+    c = a.options;
+  b = {
+    key: this.key,
+    extensions: "all",
+    s: b.s,
+    platform: b.platform,
+    appname: this.key,
+    sdkversion: b.sdkversion,
+    logversion: b.logversion,
+  };
+  c.address && (b.address = c.address);
+  c.city && (b.city = c.city);
+  c.batch && (b.batch = c.batch);
+  c.sig && (b.sig = c.sig);
+  wx.request({
+    url: "https://restapi.amap.com/v3/geocode/geo",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (d) {
+      (d = d.data) && d.status && "1" === d.status
+        ? a.success(d)
+        : a.fail({ errCode: "0", errMsg: d });
+    },
+    fail: function (d) {
+      a.fail({ errCode: "0", errMsg: d.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getRegeo = function (a) {
+  function b(d) {
+    var e = c.requestConfig;
+    wx.request({
+      url: "https://restapi.amap.com/v3/geocode/regeo",
+      data: {
+        key: c.key,
+        location: d,
+        extensions: "all",
+        s: e.s,
+        platform: e.platform,
+        appname: c.key,
+        sdkversion: e.sdkversion,
+        logversion: e.logversion,
+      },
+      method: "GET",
+      header: { "content-type": "application/json" },
+      success: function (g) {
+        if (g.data.status && "1" == g.data.status) {
+          g = g.data.regeocode;
+          var h = g.addressComponent,
+            f = [],
+            k = g.roads[0].name + "\u9644\u8fd1",
+            m = d.split(",")[0],
+            n = d.split(",")[1];
+          if (g.pois && g.pois[0]) {
+            k = g.pois[0].name + "\u9644\u8fd1";
+            var l = g.pois[0].location;
+            l &&
+              ((m = parseFloat(l.split(",")[0])),
+              (n = parseFloat(l.split(",")[1])));
+          }
+          h.provice && f.push(h.provice);
+          h.city && f.push(h.city);
+          h.district && f.push(h.district);
+          h.streetNumber && h.streetNumber.street && h.streetNumber.number
+            ? (f.push(h.streetNumber.street), f.push(h.streetNumber.number))
+            : f.push(g.roads[0].name);
+          f = f.join("");
+          a.success([
+            {
+              iconPath: a.iconPath,
+              width: a.iconWidth,
+              height: a.iconHeight,
+              name: f,
+              desc: k,
+              longitude: m,
+              latitude: n,
+              id: 0,
+              regeocodeData: g,
+            },
+          ]);
+        } else a.fail({ errCode: g.data.infocode, errMsg: g.data.info });
+      },
+      fail: function (g) {
+        a.fail({ errCode: "0", errMsg: g.errMsg || "" });
+      },
+    });
+  }
+  var c = this;
+  a.location
+    ? b(a.location)
+    : c.getWxLocation(a, function (d) {
+        b(d);
+      });
+};
+AMapWX.prototype.getWeather = function (a) {
+  function b(g) {
+    var h = "base";
+    a.type && "forecast" == a.type && (h = "all");
+    wx.request({
+      url: "https://restapi.amap.com/v3/weather/weatherInfo",
+      data: {
+        key: d.key,
+        city: g,
+        extensions: h,
+        s: e.s,
+        platform: e.platform,
+        appname: d.key,
+        sdkversion: e.sdkversion,
+        logversion: e.logversion,
+      },
+      method: "GET",
+      header: { "content-type": "application/json" },
+      success: function (f) {
+        if (f.data.status && "1" == f.data.status)
+          if (f.data.lives) {
+            if ((f = f.data.lives) && 0 < f.length) {
+              f = f[0];
+              var k = {
+                city: { text: "\u57ce\u5e02", data: f.city },
+                weather: { text: "\u5929\u6c14", data: f.weather },
+                temperature: { text: "\u6e29\u5ea6", data: f.temperature },
+                winddirection: {
+                  text: "\u98ce\u5411",
+                  data: f.winddirection + "\u98ce",
+                },
+                windpower: {
+                  text: "\u98ce\u529b",
+                  data: f.windpower + "\u7ea7",
+                },
+                humidity: { text: "\u6e7f\u5ea6", data: f.humidity + "%" },
+              };
+              k.liveData = f;
+              a.success(k);
+            }
+          } else
+            f.data.forecasts &&
+              f.data.forecasts[0] &&
+              a.success({ forecast: f.data.forecasts[0] });
+        else a.fail({ errCode: f.data.infocode, errMsg: f.data.info });
+      },
+      fail: function (f) {
+        a.fail({ errCode: "0", errMsg: f.errMsg || "" });
+      },
+    });
+  }
+  function c(g) {
+    wx.request({
+      url: "https://restapi.amap.com/v3/geocode/regeo",
+      data: {
+        key: d.key,
+        location: g,
+        extensions: "all",
+        s: e.s,
+        platform: e.platform,
+        appname: d.key,
+        sdkversion: e.sdkversion,
+        logversion: e.logversion,
+      },
+      method: "GET",
+      header: { "content-type": "application/json" },
+      success: function (h) {
+        if (h.data.status && "1" == h.data.status) {
+          h = h.data.regeocode;
+          if (h.addressComponent) var f = h.addressComponent.adcode;
+          else h.aois && 0 < h.aois.length && (f = h.aois[0].adcode);
+          b(f);
+        } else a.fail({ errCode: h.data.infocode, errMsg: h.data.info });
+      },
+      fail: function (h) {
+        a.fail({ errCode: "0", errMsg: h.errMsg || "" });
+      },
+    });
+  }
+  var d = this,
+    e = d.requestConfig;
+  a.city
+    ? b(a.city)
+    : d.getWxLocation(a, function (g) {
+        c(g);
+      });
+};
+AMapWX.prototype.getPoiAround = function (a) {
+  function b(e) {
+    e = {
+      key: c.key,
+      location: e,
+      s: d.s,
+      platform: d.platform,
+      appname: c.key,
+      sdkversion: d.sdkversion,
+      logversion: d.logversion,
+    };
+    a.querytypes && (e.types = a.querytypes);
+    a.querykeywords && (e.keywords = a.querykeywords);
+    wx.request({
+      url: "https://restapi.amap.com/v3/place/around",
+      data: e,
+      method: "GET",
+      header: { "content-type": "application/json" },
+      success: function (g) {
+        if (g.data.status && "1" == g.data.status) {
+          if ((g = g.data) && g.pois) {
+            for (var h = [], f = 0; f < g.pois.length; f++) {
+              var k = 0 == f ? a.iconPathSelected : a.iconPath;
+              h.push({
+                latitude: parseFloat(g.pois[f].location.split(",")[1]),
+                longitude: parseFloat(g.pois[f].location.split(",")[0]),
+                iconPath: k,
+                width: 22,
+                height: 32,
+                id: f,
+                name: g.pois[f].name,
+                address: g.pois[f].address,
+              });
+            }
+            a.success({ markers: h, poisData: g.pois });
+          }
+        } else a.fail({ errCode: g.data.infocode, errMsg: g.data.info });
+      },
+      fail: function (g) {
+        a.fail({ errCode: "0", errMsg: g.errMsg || "" });
+      },
+    });
+  }
+  var c = this,
+    d = c.requestConfig;
+  a.location
+    ? b(a.location)
+    : c.getWxLocation(a, function (e) {
+        b(e);
+      });
+};
+AMapWX.prototype.getStaticmap = function (a) {
+  function b(e) {
+    c.push("location=" + e);
+    a.zoom && c.push("zoom=" + a.zoom);
+    a.size && c.push("size=" + a.size);
+    a.scale && c.push("scale=" + a.scale);
+    a.markers && c.push("markers=" + a.markers);
+    a.labels && c.push("labels=" + a.labels);
+    a.paths && c.push("paths=" + a.paths);
+    a.traffic && c.push("traffic=" + a.traffic);
+    e = "https://restapi.amap.com/v3/staticmap?" + c.join("&");
+    a.success({ url: e });
+  }
+  var c = [];
+  c.push("key=" + this.key);
+  var d = this.requestConfig;
+  c.push("s=" + d.s);
+  c.push("platform=" + d.platform);
+  c.push("appname=" + d.appname);
+  c.push("sdkversion=" + d.sdkversion);
+  c.push("logversion=" + d.logversion);
+  a.location
+    ? b(a.location)
+    : this.getWxLocation(a, function (e) {
+        b(e);
+      });
+};
+AMapWX.prototype.getInputtips = function (a) {
+  var b = Object.assign({}, this.requestConfig);
+  a.location && (b.location = a.location);
+  a.keywords && (b.keywords = a.keywords);
+  a.type && (b.type = a.type);
+  a.city && (b.city = a.city);
+  a.citylimit && (b.citylimit = a.citylimit);
+  wx.request({
+    url: "https://restapi.amap.com/v3/assistant/inputtips",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (c) {
+      c && c.data && c.data.tips && a.success({ tips: c.data.tips });
+    },
+    fail: function (c) {
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getDrivingRoute = function (a) {
+  var b = Object.assign({}, this.requestConfig);
+  a.origin && (b.origin = a.origin);
+  a.destination && (b.destination = a.destination);
+  a.strategy && (b.strategy = a.strategy);
+  a.waypoints && (b.waypoints = a.waypoints);
+  a.avoidpolygons && (b.avoidpolygons = a.avoidpolygons);
+  a.avoidroad && (b.avoidroad = a.avoidroad);
+  wx.request({
+    url: "https://restapi.amap.com/v3/direction/driving",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (c) {
+      c &&
+        c.data &&
+        c.data.route &&
+        a.success({
+          paths: c.data.route.paths,
+          taxi_cost: c.data.route.taxi_cost || "",
+        });
+    },
+    fail: function (c) {
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getWalkingRoute = function (a) {
+  var b = Object.assign({}, this.requestConfig);
+  a.origin && (b.origin = a.origin);
+  a.destination && (b.destination = a.destination);
+  wx.request({
+    url: "https://restapi.amap.com/v3/direction/walking",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (c) {
+      c && c.data && c.data.route && a.success({ paths: c.data.route.paths });
+    },
+    fail: function (c) {
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getTransitRoute = function (a) {
+  var b = Object.assign({}, this.requestConfig);
+  a.origin && (b.origin = a.origin);
+  a.destination && (b.destination = a.destination);
+  a.strategy && (b.strategy = a.strategy);
+  a.city && (b.city = a.city);
+  a.cityd && (b.cityd = a.cityd);
+  wx.request({
+    url: "https://restapi.amap.com/v3/direction/transit/integrated",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (c) {
+      c &&
+        c.data &&
+        c.data.route &&
+        ((c = c.data.route),
+        a.success({
+          distance: c.distance || "",
+          taxi_cost: c.taxi_cost || "",
+          transits: c.transits,
+        }));
+    },
+    fail: function (c) {
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+AMapWX.prototype.getRidingRoute = function (a) {
+  var b = Object.assign({}, this.requestConfig);
+  a.origin && (b.origin = a.origin);
+  a.destination && (b.destination = a.destination);
+  wx.request({
+    url: "https://restapi.amap.com/v3/direction/riding",
+    data: b,
+    method: "GET",
+    header: { "content-type": "application/json" },
+    success: function (c) {
+      c && c.data && c.data.route && a.success({ paths: c.data.route.paths });
+    },
+    fail: function (c) {
+      a.fail({ errCode: "0", errMsg: c.errMsg || "" });
+    },
+  });
+};
+
+export default {
+  AMapWX
+}

+ 19 - 0
src/main.ts

@@ -0,0 +1,19 @@
+import App from './App.vue';
+import { createSSRApp } from 'vue';
+import { createPinia } from 'pinia';
+import { registryConvert } from './common/utils/ConvertRgeistry';
+
+import DialogAction from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
+import PageAction from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+
+export function createApp() {
+  const app = createSSRApp(App);
+  const pinia = createPinia()
+  app.use(DialogAction);
+  app.use(PageAction);
+  app.use(pinia);
+  registryConvert();
+  return {
+    app
+  }
+}

+ 95 - 0
src/manifest.json

@@ -0,0 +1,95 @@
+{
+    "name" : "xiangyuan-app",
+    "appid" : "__UNI__971AA7D",
+    "description" : "乡源·乡村文化资源挖掘平台",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wx57b317f69760fe9e",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "postcss" : true,
+            "minified" : true,
+            "ignoreDevUnusedFiles" : false
+        },
+        "permission" : {
+            "scope.userLocation" : {
+                "desc" : "用于标识志愿者的位置信息"
+            }
+        },
+        "requiredPrivateInfos" : [ "getLocation", "chooseAddress", "chooseLocation", "choosePoi" ],
+        "plugins" : {}
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3",
+    "h5" : {
+        "sdkConfigs" : {
+            "maps" : {
+                "qqmap" : {
+                    "key" : "LDXBZ-JIWWC-IXW2S-AUDZS-26VC2-GRBC4"
+                }
+            }
+        },
+        "router" : {
+            "mode" : "hash",
+            "base" : "./"
+        }
+    }
+}

+ 159 - 0
src/pages.json

@@ -0,0 +1,159 @@
+{
+  "easycom": {
+    "^u-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
+    "^up-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
+  },
+  "pages": [
+    {
+      "path" : "pages/dig/index",
+      "style" :
+      {
+        "navigationBarTitleText" : "乡源·乡村文化资源挖掘平台",
+        "enablePullDownRefresh" : false
+      }
+    },
+    {
+      "path": "pages/user/index",
+      "style": {
+        "navigationBarTitleText": "个人中心",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/user/login",
+      "style": {
+        "navigationBarTitleText": "登录",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/article/editor/editor",
+      "style": {
+        "navigationBarTitleText": "编辑文章",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/article/editor/preview",
+      "style": {
+        "navigationBarTitleText": "预览文章",
+        "enablePullDownRefresh": false
+      }
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTextStyle": "white",
+    "navigationBarTitleText": "uni-app",
+    "navigationBarBackgroundColor": "#FF8719",
+    "backgroundColor": "#F8F8F8"
+  },
+  "subPackages": [
+    {
+      "root": "pages/dig",
+      "pages": [
+        {
+          "path": "details",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-详情",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/environment",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-环境格局",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/food",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-地道美食",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/mine",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-物产资源",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/summary",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-村落概况",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/building",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-传统建筑",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/trip",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-旅游路线",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/custom",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-民俗文化",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "task/history",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-历史文化",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/common",
+          "style": {
+            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-提交信息",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/village_claim",
+          "style": {
+            "navigationBarTitleText": "认领村庄",
+            "enablePullDownRefresh": false
+          }
+        },
+        {
+          "path": "forms/list",
+          "style": {
+            "navigationBarTitleText": "信息列表",
+            "enablePullDownRefresh": false
+          }
+        }
+      ]
+    }
+  ],
+  "uniIdRouter": {},
+  "tabBar": {
+    "selectedColor": "#FF8719",
+    "list": [
+      {
+        "pagePath": "pages/dig/index",
+        "iconPath": "https://mn.wenlvti.net/uploads/20250313/ff0545051683c42559f51743e839f8c4.png",
+        "selectedIconPath": "https://mn.wenlvti.net/uploads/20250313/03e58ca265365d90339b04dc1eeff279.png",
+        "text": "村史"
+      },
+      {
+        "pagePath": "pages/user/index",
+        "iconPath": "https://mn.wenlvti.net/uploads/20250313/7fd9655fb996786d8624fc46b90c3210.png",
+        "selectedIconPath": "https://mn.wenlvti.net/uploads/20250313/034d699de291d1b7192b45a4493b0815.png",
+        "text": "我的"
+      }
+    ]
+  }
+}

+ 106 - 0
src/pages/article/editor/editor.vue

@@ -0,0 +1,106 @@
+<template>
+  <view class="d-flex flex-column h-100vh">
+    <sp-editor
+      :toolbar-config="{
+        excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'],
+        iconSize: '18px'
+      }"
+      @init="initEditor"
+      @input="inputOver"
+      @upinImage="upinImage"
+      @overMax="overMax"
+    ></sp-editor>
+    
+    <view class="d-flex flex-row align-center gap-s p-3">
+      <u-button @click="cancel">取消</u-button>
+      <u-button type="primary" @click="save">保存</u-button>
+    </view>
+    <u-safe-bottom />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { confirm } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
+import { back, backAndCallOnPageBack } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import spEditor from '@/uni_modules/sp-editor/components/sp-editor/sp-editor.vue';
+
+function cancel() {
+  confirm({
+    title: '提示',
+    content: '是否放弃编辑?',
+  }).then((res) => {
+    if (res)
+      back();
+  })
+}
+function save() {
+  console.log('save', currentContent);
+  
+  uni.setStorage({
+    key: 'editorContent',
+    data: currentContent,
+    success: () => backAndCallOnPageBack('editor', {}),
+    fail: (e) => showError(e),
+  })
+}
+
+let currentContent = '';
+
+/**
+* 获取输入内容
+*/
+function inputOver(e: { html: string; text: string; }) {
+  // 可以在此处获取到编辑器已编辑的内容
+  currentContent = e.html;
+}
+/**
+ * 超出最大内容限制
+ * @param {Object} e {html,text} 内容的html文本,和text文本
+ */
+function overMax(e: { html: string; text: string; }) {
+  // 若设置了最大字数限制,可在此处触发超出限制的回调
+  console.log('==== overMax :', e)
+}
+function initEditor(editor: any) {
+  uni.getStorage({
+    key: 'editorContent',
+    success: (success) => {
+      editor.setContents({
+        html: success.data
+      })
+    },
+  })
+}
+/**
+ * 直接运行示例工程插入图片无法正常显示的看这里
+ * 因为插件默认采用云端存储图片的方式
+ * 以$emit('upinImage', tempFiles, this.editorCtx)的方式回调
+ * @param {Object} tempFiles
+ * @param {Object} editorCtx
+ */
+function upinImage(tempFiles: any, editorCtx: any) {
+  /**
+   * 本地临时插入图片预览
+   * 注意:这里仅是示例本地图片预览,因为需要将图片先上传到云端,再将图片插入到编辑器中
+   * 正式开发时,还请将此处注释,并解开下面 使用 uniCloud.uploadFile 上传图片的示例方法 的注释
+   * @tutorial https://uniapp.dcloud.net.cn/api/media/editor-context.html#editorcontext-insertimage
+   */
+  // #ifdef MP-WEIXIN
+  // 注意微信小程序的图片路径是在tempFilePath字段中
+  editorCtx.insertImage({
+    src: tempFiles[0].tempFilePath,
+    width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
+    success: function () {}
+  })
+  // #endif
+
+  // #ifndef MP-WEIXIN
+  editorCtx.insertImage({
+    src: tempFiles[0].path,
+    width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
+    success: function () {}
+  })
+  // #endif
+}
+</script>

+ 23 - 0
src/pages/article/editor/preview.vue

@@ -0,0 +1,23 @@
+<template>
+  <u-empty v-if="!content" mode="news" text="空内容,请先编写内容后再预览" />
+  <view v-else class="p-3">
+    <u-parse :content="content" :tagStyle="commonParserStyle"></u-parse>
+  </view>
+</template>
+
+<script setup lang="ts">
+import commonParserStyle from '@/common/style/commonParserStyle';
+import { onLoad } from '@dcloudio/uni-app';
+import { ref } from 'vue';
+
+const content = ref();
+
+onLoad(() => {
+  uni.getStorage({
+    key: 'editorContent',
+    success: (success) => {
+      content.value = success.data;
+    },
+  })
+})
+</script>

+ 23 - 0
src/pages/dig/composeable/TaskEntryForm.ts

@@ -0,0 +1,23 @@
+import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
+import { navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
+
+export function useTaskEntryForm() {
+  const { querys } = useLoadQuerys({ 
+    villageId: 0,  
+    villageVolunteerId: 0,
+  });
+  
+  function goForm(subType: string, subId: number, subKey = 'type', type = 'list') {
+    navTo('../forms/' + type, {
+      villageId: querys.value.villageId,  
+      villageVolunteerId: querys.value.villageVolunteerId,  
+      subType,
+      subId,
+      subKey,
+    })
+  }
+
+  return {
+    goForm,
+  }
+}

+ 253 - 0
src/pages/dig/details.vue

@@ -0,0 +1,253 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_1.jpg"></image>
+    </view>
+    <view class="level-info">
+      <view>
+        <view class="label">已认领:</view>
+        <view class="value">{{ querys.name }}
+          <text class="em">Lv.{{ querys.level }}</text>
+        </view>
+      </view>
+      <view>
+        <view class="label">文化积分:</view>
+        <view class="value">{{ querys.points }}</view>
+      </view>
+      <view class="btn">
+        <text class="iconfont icon-point"></text>
+        积分兑换
+      </view>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-summary"></text>
+        <view class="info">
+          <view class="title">村落概况</view>
+          <view class="desc">探索村落的历史渊源与发生轨迹</view>
+        </view>
+        <view class="btn" @click="navTo('task/summary', nextPageData)">去完成</view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history"></text>
+        <view class="info">
+          <view class="title">历史文化</view>
+          <view class="desc">传承百年文化遗产和精神财富</view>
+        </view>
+        <view class="btn" @click="navTo('task/history', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-environment"></text>
+        <view class="info">
+          <view class="title">环境格局</view>
+          <view class="desc">感受自然人文环境之美</view>
+        </view>
+        <view class="btn" @click="navTo('task/environment', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-building"></text>
+        <view class="info">
+          <view class="title">传统建筑</view>
+          <view class="desc">领略古建筑的独特魅力</view>
+        </view>
+        <view class="btn" @click="navTo('task/building', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom"></text>
+        <view class="info">
+          <view class="title">民俗文化</view>
+          <view class="desc">体验民间传统习俗与节庆</view>
+        </view>
+        <view class="btn" @click="navTo('task/custom', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-food"></text>
+        <view class="info">
+          <view class="title">地道美食</view>
+          <view class="desc">正宗、传统地方特色美食</view>
+        </view>
+        <view class="btn" @click="navTo('task/food', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-mine"></text>
+        <view class="info">
+          <view class="title">物产资源</view>
+          <view class="desc">特定地域的植物、矿物或手工艺</view>
+        </view>
+        <view class="btn" @click="navTo('task/mine', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-trip"></text>
+        <view class="info">
+          <view class="title">旅游路线</view>
+          <view class="desc">体验独特的文化魅力</view>
+        </view>
+        <view class="btn" @click="navTo('task/trip', nextPageData)">
+          去完成
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-other"></text>
+        <view class="info">
+          <view class="title">其他</view>
+          <view class="desc">更多文化传承相关信息</view>
+        </view>
+        <view class="btn active">
+          待开放
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import { computed } from 'vue';
+
+const { querys } = useLoadQuerys({ 
+  id: 0,  
+  name: '',
+  points: 0,
+  level: 0,
+  villageVolunteerId: 0,
+});
+
+const nextPageData = computed(() => ({
+  villageId: querys.value.id,  
+  villageVolunteerId: querys.value.villageVolunteerId,
+}));
+
+function goForm(subType: string, subId: number) {
+  navTo('../forms/list', {
+    villageId: querys.value.id,  
+    villageVolunteerId: querys.value.villageVolunteerId,  
+    subType,
+    subId,
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.img-banner {
+  height: 246rpx;
+  width: 100%;
+
+  image {
+    height: 100%;
+    width: 100%;
+  }
+}
+
+.level-info {
+  padding: 48rpx 36rpx 36rpx;
+  background: #FFF2E6;
+  border-radius: 20rpx;
+  position: relative;
+  margin-top: 42rpx;
+  margin-bottom: 37rpx;
+  > view:first-child {
+    margin-bottom: 44rpx;
+  }
+
+  .label {
+    font-size: 28rpx;
+    color: #111111;
+    display: inline-block;
+  }
+
+  .value {
+    font-weight: bold;
+    font-size: 28rpx;
+    display: inline-block;
+    color: #333333;
+
+    .em {
+      font-family: Rockwell;
+      font-weight: 600;
+      font-size: 30rpx;
+      color: #FF8719;
+      display: inline-block;
+      margin-left: 35rpx;
+    }
+  }
+
+  .btn {
+    position: absolute;
+    top: 30rpx;
+    right: 30rpx;
+    border-radius: 10rpx;
+    border: 1px solid #FF8719;
+    padding: 15rpx 20rpx;
+    display: flex;
+    align-items: center;
+    font-weight: 400;
+    font-size: 28rpx;
+    color: #FF8719;
+
+    text.iconfont {
+      display: inline-block;
+      margin-right: 15rpx;
+      font-size: 40rpx;
+    }
+  }
+}
+.task-list{
+  .item{
+    display: flex;
+    align-items: center;
+    background: #fff;
+    margin-bottom: 36rpx;
+    padding:39rpx 27rpx 38rpx;
+    text.iconfont{
+      width: 91rpx;
+      height: 91rpx;
+      border-radius: 50%;
+      border: 1px solid #25515E;
+      text-align: center;
+      color: #25515E;
+      font-size: 60rpx;
+      line-height: 91rpx;
+      display: inline-block;
+      margin-right: 17rpx;
+    }
+    .btn{
+      background: #FF8719;
+      border-radius: 28rpx;
+      color:#fff;
+      font-size: 28rpx;
+      padding:14rpx 24rpx;
+      &.active{
+        background: #EFEFEF;
+        color:#999999;
+      }
+    }
+    .info{
+      flex:1;
+      .title{
+        font-weight: 600;
+        font-size: 30rpx;
+        color: #333333;
+        margin-bottom: 22rpx;
+      }
+      .desc{
+        font-weight: 400;
+        font-size: 24rpx;
+        color: #999999;
+      }
+    }
+  }
+}
+</style>

+ 170 - 0
src/pages/dig/forms/common.vue

@@ -0,0 +1,170 @@
+<template>
+  <view class="main">
+    <u-loading-page :loading="loading" /> 
+    <SimpleDynamicFormUni
+      ref="formRef"
+      :formDefine="formDefine"
+      :formProps="{
+        labelPosition: 'top',
+        labelWidth: '720rpx',
+      }"
+      :formGlobalParams="querys"
+    />
+    <u-button type="primary" @click="submit">提交</u-button>
+    <u-safe-bottom />
+    <Success ref="popupRef" @back="backPrev" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { getVillageInfoForm } from './forms';
+import { backAndCallOnPageBack } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import type { FormDefine, FormExport } from '@/common/components/form';
+import VillageInfoApi, { CommonInfoModel } from '@/api/inhert/VillageInfoApi';
+import SimpleDynamicFormUni from '@/common/components/form/SimpleDynamicFormUni.vue';
+import Success from './success.vue';
+import { RequestApiError } from '@imengyu/imengyu-utils/dist/request';
+
+const popupRef = ref();
+const loading = ref(false);
+
+const formRef = ref<FormExport>();
+const formDefine = ref<FormDefine>();
+
+async function submit() {
+  if (!formRef.value)
+    return;
+  try {
+    const data = await formRef.value.submitForm<CommonInfoModel>();
+    if (!data)
+      return;
+    loading.value = true;
+    data.type = querys.value.subId;
+    await VillageInfoApi.updateInfo(
+      querys.value.subType,
+      querys.value.villageId,
+      querys.value.villageVolunteerId,
+      data,
+    );
+    popupRef.value.open();
+  } catch (e) {
+    showError(e);
+  } finally {
+    loading.value = false;
+  }
+}
+function backPrev(needRefresh: boolean) {
+  backAndCallOnPageBack('list', {
+    needRefresh,
+  });
+}
+
+const { querys } = useLoadQuerys({ 
+  villageId: 0,  
+  villageVolunteerId: 0,
+  subType: '',
+  subId: 0,
+  id: 0,
+}, async (querys) => {
+  loading.value = true;
+  if (!formRef.value)
+    return;
+
+  let formData = undefined;
+
+  try {
+    const [model, forms] = getVillageInfoForm(querys.subType, querys.subId);
+    formRef.value.initFormData(() => new model());
+    formDefine.value = forms;
+    if (querys.id >= 0)
+      formData = await VillageInfoApi.getInfo(
+        querys.subType, 
+        querys.subId,
+        querys.villageId, 
+        querys.villageVolunteerId,
+        querys.id,
+        model,
+      );
+  } catch (e) {
+    if (!(e instanceof RequestApiError && e.errorMessage.startsWith('请完成')))
+      showError(e, undefined, () => backPrev(false));
+  } finally {
+    loading.value = false;
+  }
+
+  if (formData) {
+    formRef.value.loadFormData(formData);
+  }
+});
+
+
+</script>
+
+<style lang="scss" scoped>
+.form-block {
+  margin-bottom: 32rpx;
+  padding: 24rpx 26rpx;
+  background: #fff;
+  border-radius: 10rpx;
+}
+
+::v-deep .uni-forms-item__label {
+  font-weight: 600;
+  font-size: 28rpx;
+  color: #23262D;
+  line-height: 36rpx;
+}
+::v-deep .uni-input-placeholder,::v-deep .uni-textarea-placeholder, ::v-deep .uni-select__input-placeholder, ::v-deep .uni-date__x-input, ::v-deep .is-disabled .uni-easyinput__placeholder-class{
+  font-weight: 400;
+  font-size: 28rpx;
+  color: #999999;
+}
+::v-deep .uni-easyinput__content.is-disabled {
+  background-color: #fff;
+  cursor: pointer;
+}
+button[type="primary"] {
+  background: #FF8719;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  padding: 8rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+.address-select {
+  position: relative;
+  width: 100%;  // 添加宽度
+  height: 100%;
+}
+.input-wrapper {
+  width: 100%;
+  height: 100%;
+  pointer-events: none;  /* 禁用内部元素的点击事件 */
+}
+::v-deep .uni-easyinput__content.is-disabled {
+  background-color: #fff;
+  cursor: pointer;
+  width: 100%;  // 确保输入框宽度占满
+}
+::v-deep .popup-content{
+  text-align: center;
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 22rpx 44rpx 37rpx;
+  image{
+    width: 158rpx;
+    height:186rpx;
+    display: block;
+    margin: 0 auto;
+    margin-bottom: 12rpx;
+  }
+  text{
+    font-weight: 500;
+    font-size: 24rpx;
+    color: #666666;
+  }
+}
+</style>

File diff suppressed because it is too large
+ 2426 - 0
src/pages/dig/forms/forms.ts


+ 135 - 0
src/pages/dig/forms/list.vue

@@ -0,0 +1,135 @@
+<template>
+  <view class="article_list">
+    <view class="search with-button">
+      <uni-search-bar 
+        v-model="searchText"
+        radius="100"
+        bgColor="#fff"
+        placeholder="搜一搜" 
+        clearButton="auto"
+        cancelButton="none"
+        @confirm="search"
+      />
+      <u-button type="primary" @click="newData">+ 新增</u-button>
+    </view>
+    <view class="complex-list-horizontal-1">
+      <view 
+        class="item" 
+        hover-class="pressed"
+        v-for="item in listLoader.list.value"
+        :key="item.id" 
+        @click="goDetail(item.id)"
+      >
+        <ImageWrapper :src="item.image" width="170rpx" height="190rpx" />
+        <view class="info">
+          <view class="name ellipsis-2">{{ item.title }}</view>
+          <view class="desc">{{ item.date }}</view>
+        </view>
+      </view>
+    </view>
+    <SimplePageListLoader :loader="listLoader" :noEmpty="true">
+      <template #empty>
+        <u-empty mode="list" text="暂无数据,点击按钮新增数据">
+          <u-gap height="20"></u-gap>
+          <u-button type="primary" @click="newData">+ 新增数据</u-button>
+        </u-empty>
+      </template>
+    </SimplePageListLoader>
+  </view>
+</template>
+
+<script setup lang="ts">
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import ImageWrapper from '@/common/components/ImageWrapper.vue';
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { ref } from 'vue';
+import { DataDateUtils } from '@imengyu/js-request-transform';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import VillageInfoApi from '@/api/inhert/VillageInfoApi';
+
+const searchText = ref('');
+const listLoader = useSimplePageListLoader<{
+  id: number,
+  image: string,
+  title: string,
+  date: string
+}, {
+  villageId: number,  
+  villageVolunteerId: number,
+  subType: string,
+  subId: number,
+  subKey: string,
+}>(8, async (page, pageSize, params) => {
+  if (!params || !params.subType || !params.villageId || !params.villageVolunteerId)
+    throw new Error("未传入参数,当前页面需要参数");
+  let res = (page == 1 ? await VillageInfoApi.getList(
+    params.subType,
+    params.subId,
+    params.subKey,
+    params.villageId,
+    params.villageVolunteerId,
+  ) : [])
+  if (searchText.value)
+    res = res.filter((p) => p.title.includes(searchText.value));
+  const list = res.map((item) => {
+    return {
+      id: item.id,
+      image: item.image,
+      title: item.title,
+      date: DataDateUtils.formatDate(item.updatedAt, 'YYYY-MM-dd'),
+    }
+  })
+  return {
+    list: list,
+    total: list.length,
+  };
+});
+
+function newData() {
+  navTo('common', { 
+    id: -1,
+    villageId: querys.value.villageId,  
+    villageVolunteerId: querys.value.villageVolunteerId,  
+    subType: querys.value.subType,  
+    subId: querys.value.subId,  
+  });
+}
+function goDetail(id: number) {
+  navTo('common', { 
+    id,
+    villageId: querys.value.villageId,
+    villageVolunteerId: querys.value.villageVolunteerId,
+    subType: querys.value.subType,
+    subKey: querys.value.subKey,
+    subId: querys.value.subId,
+  });
+}
+function search() {
+  listLoader.loadData(undefined, true);
+}
+
+
+const { querys } = useLoadQuerys({ 
+  villageId: 0,  
+  villageVolunteerId: 0,
+  subType: '',
+  subKey: '',
+  subId: 0,
+}, async (querys) => {
+  listLoader.loadData(querys)
+});
+
+defineExpose({
+  onPageBack(name: string, param: any) {
+    if (param && param.needRefresh)
+      listLoader.loadData(undefined, true);
+  }
+})
+</script>
+
+<style lang="scss">
+.article_list {
+  padding: 20rpx;
+}
+</style>

+ 26 - 0
src/pages/dig/forms/success.vue

@@ -0,0 +1,26 @@
+<template>
+  <uni-popup ref="popupRef">
+    <view class="popup-content">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/uploads/20250313/46adb2f039c6f23a3e69149526eb7e61.png">
+      </image>
+      <text class="text">信息已提交,请耐心等待审核结果</text>
+      <view class="mt-3">
+        <u-button type="primary" @click="$emit('back', true)">好的</u-button>
+      </view>
+    </view>
+  </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const popupRef = ref();
+
+defineEmits([ 'back' ])
+defineExpose({
+  open: () => {
+    popupRef.value.open();
+  },
+});
+
+</script>

+ 176 - 0
src/pages/dig/forms/village_claim.vue

@@ -0,0 +1,176 @@
+<template>
+  <view class="main">
+    <u-loading-page :loading="loading" /> 
+    <SimpleDynamicFormUni
+      ref="formRef"
+      :formDefine="formDefine"
+      :formProps="{
+        labelPosition: 'top',
+        labelWidth: '175',
+      }"
+    />
+    <button class="primary" @click="submit">提交</button>
+    <u-safe-bottom />
+    <Success ref="popupRef" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { ref } from 'vue';
+import VillageApi from '@/api/inhert/VillageApi';
+import CheckUtils from '@imengyu/imengyu-utils/dist/CheckUtils';
+import type { FormDefine, FormExport } from '@/common/components/form';
+import type { DynamicSelectProps } from '@/common/components/form/components/DynamicSelect.vue';
+import SimpleDynamicFormUni from '@/common/components/form/SimpleDynamicFormUni.vue';
+import Success from './success.vue';
+
+const formRef = ref<FormExport>();
+const popupRef = ref();
+const loading = ref(false);
+
+const formDefine : FormDefine = {
+  items: [
+    {
+      name: '',
+      children: {
+        type: 'group',
+        props: {
+          class: 'form-block',
+        },
+        propNestType: 'nest', 
+        items: [
+          { 
+            label: '认领村庄', 
+            name: 'village_id',
+            type: 'dynamic-select', 
+            params: {
+              loadData: async () => 
+              (await VillageApi.getCanClaimVallageList())
+                .map((p) => ({
+                  value: p.id,
+                  text: p.villageName,
+                }))
+              ,
+            } as DynamicSelectProps,
+            rules: [{
+              required: true,
+              errorMessage: '请选择要认领的村庄',
+            }],
+          },
+          { 
+            label: '姓名', 
+            name: 'name', 
+            type: 'text', 
+            defaultValue: '',
+            params: {
+              placeholder: '请输入姓名',
+            },
+            rules:  [{
+              required: true,
+              errorMessage: '请输入姓名',
+            }] 
+          },
+          { 
+            label: '性别', 
+            name: 'sex', 
+            type: 'select', 
+            params: {
+              localdata: [
+                { value: 1, text: "男" },
+                { value: 2, text: "女" },
+                { value: 3, text: "不透露" },
+              ],
+              clear: false,
+            },
+            rules: [{
+              required: true,
+              errorMessage: '请选择性别',
+            }]
+          },
+          {
+            label: '联系方式',
+            name: 'mobile',
+            type: 'text', 
+            defaultValue: '',
+            params: {
+              placeholder: '请输入联系方式',
+            },
+            rules:  [{
+              required: true,
+              errorMessage: '请输入手机号',
+            },{
+              required: true,
+              validateFunction: (rule: any, value: any,data: any,callback: (e: any) => void) => {
+                if (!CheckUtils.checkIsChinesePhoneNumber(value)) {
+                  callback('手机号格式不正确')
+                  return false
+                }
+                return true
+              }
+            }]
+          },
+          {
+            label: '地址',
+            name: 'address',
+            type: 'text', 
+            defaultValue: '',
+            params: {
+              placeholder: '请输入地址',
+            },
+            rules: [{
+              required: true,
+              errorMessage: '请输入地址',
+            }]
+          },
+        ]
+      }
+    },
+    {
+      name: '',
+      children: {
+        type: 'group',
+        props: {
+          class: 'form-block',
+        },
+        propNestType: 'nest', 
+        items: [
+          {
+            label: '申请理由',
+            name: 'claim_reason',
+            type: 'textarea',
+            defaultValue: '',
+            params: {
+              placeholder: '请输入申请理由',
+            },
+            rules: [{
+              required: true,
+              errorMessage: '请输入申请理由',
+            }]
+          }
+        ]
+      }
+    },
+  ]
+}
+
+async function submit() {
+  if (!formRef.value)
+    return;
+  try {
+    const data = await formRef.value.submitForm();
+    if (!data)
+      return;
+    loading.value = true;
+    await VillageApi.claimVallage(data);
+    popupRef.value.open();
+  } catch (e) {
+    showError(e);
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style lang="scss">
+</style>

+ 251 - 0
src/pages/dig/index.vue

@@ -0,0 +1,251 @@
+<template>
+	<view class="submit_main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_submit.jpg"></image>
+    </view>
+    <view class="main">
+      <template v-if="!villageListLoader.content.value?.length">
+        <view class="cat">
+          <text>村庄认领</text>
+        </view>
+        <u-button type="primary" @click="goClamVillage">
+          <text>+</text>
+          认领新村庄
+        </u-button>
+      </template>
+      <view class="cat">
+        <text>已认领村庄</text>
+      </view>
+      <RequireLogin unLoginMessage="登录后查看我认领的村庄">
+        <SimplePageContentLoader
+          :loader="villageListLoader"
+          :showEmpty="villageListLoader.content.value?.length == 0"
+          :emptyView="{
+            text: '你还没有认领的村庄',
+            button: true,
+            buttonText: '去认领',
+            buttonClick: goClamVillage,
+          }"
+        >
+          <view class="village-list">
+            <view 
+              v-for="item in villageListLoader.content.value"
+              :key="item.id"
+              class="item"
+            >
+              <ImageWrapper 
+                mode="aspectFill"
+                :src="item.image" 
+                width="154rpx"
+                height="154rpx"
+              />
+              <view class="info">
+                <view class="name">{{ item.villageName }}</view>
+                <view class="d-flex flex-row align-center">
+                  <view class="btn p-1 pl-2 pr-2" @click="navTo('/pages/inherit/village/details', { id: item.villageId })">
+                    <text class="iconfont icon-view"></text>预览
+                  </view>
+                  <view class="btn p-1 pl-2 pr-2 active" @click="goSubmitDigPage(item)">
+                    <text class="iconfont icon-search"></text>采编
+                  </view>
+                </view>
+              </view>
+            </view>
+          </view>
+        </SimplePageContentLoader>
+      </RequireLogin>
+
+      <view class="cat">
+        <text>我的贡献</text>
+      </view>
+      <RequireLogin unLoginMessage="登录后贡献,加入排行榜">
+        <view class="retribution">
+          <view class="item">
+            <text class="iconfont icon-total-points"></text>
+            <view class="num">{{ volunteerInfoLoader.content.value?.points }}</view>
+            <view>文化积分</view>
+          </view>
+          <view>
+            <text class="iconfont icon-art-arrow"></text>
+          </view>
+          <view class="item">
+            <text class="iconfont icon-level"></text>
+            <view class="level">Lv.{{ volunteerInfoLoader.content.value?.level }}</view>
+            <view>等级</view>
+          </view>
+        </view>
+      </RequireLogin>
+    </view>
+	</view>
+</template>
+
+<script setup lang="ts">
+import VillageApi, { VillageListItem } from '@/api/inhert/VillageApi';
+import ImageWrapper from '@/common/components/ImageWrapper.vue';
+import RequireLogin from '@/common/components/RequireLogin.vue';
+import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
+import { useReqireLogin } from '@/common/composeabe/RequireLogin';
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+
+const villageListLoader = useSimpleDataLoader(async () => await VillageApi.getClaimedVallageList(), true);
+const volunteerInfoLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerInfo(), true);
+const rankListLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerRanklist(), true);
+
+const { requireLogin } = useReqireLogin();
+
+function goClamVillage() {
+  requireLogin(() => navTo('forms/village_claim'), '登录后才能认领村庄哦!');
+}
+function goSubmitDigPage(item: VillageListItem) {
+  navTo('details', { 
+    id: item.villageId,
+    name: item.villageName,
+    villageVolunteerId: item.villageVolunteerId,
+    points: volunteerInfoLoader.content.value?.points,
+    level: volunteerInfoLoader.content.value?.level,
+  })
+}
+</script>
+
+<style lang="scss">
+.submit_main {
+  .img-banner{
+    border-radius: 0;
+    height: 466rpx;
+  }
+  .cat{
+    font-weight: 600;
+    display: flex;
+    margin: 36rpx 0 40rpx;
+    align-items: center;
+    text{
+      display: block;
+      font-size: 36rpx;
+      color: #312520;
+      flex:1;
+    }
+    .btn{
+      border-radius: 28rpx;
+      border: 1px solid #999999;
+      font-size: 27rpx;
+      color: #666666;
+      padding:15rpx 28rpx;
+      display: inline-block;
+      margin-left: 18rpx;
+      &.active{
+        color:#fff;
+        background: #FF8719;
+        border-color: #FF8719;
+      }
+    }
+  }
+  .village-list{
+    .item{
+      background: #FFFFFF;
+      border-radius: 10rpx;
+      padding:18rpx 14rpx 11rpx;
+      display: flex;
+      margin-bottom: 27rpx;
+      image,.image-wrapper {
+        border-radius: 10rpx;
+        width: 154rpx;
+        height: 154rpx;
+        display: block;
+        margin-right: 25rpx;
+        overflow: hidden;
+      }
+      .info{
+        .name{
+          font-size: 30rpx;
+          color: #333333;
+          margin-bottom: 35rpx;
+          margin-top: 14rpx;
+        }
+        .btn{
+          border-radius: 10rpx;
+          border: 1px solid #25515E;
+          padding:16rpx 44rpx;
+          font-size: 28rpx;
+          color:#25515E;
+          margin-right: 34rpx;
+          display: inline-block;
+          &.active{
+            background: #FF8719;
+            color:#fff;
+            border-color: #FF8719;
+          }
+          text.iconfont{
+            display: inline-block;
+            margin-right: 15rpx;
+            font-size: 36rpx;
+          }
+        }
+      }
+    }
+  }
+  .retribution{
+    background: #FFFFFF;
+    border-radius: 10rpx;
+    display: flex;
+    justify-content: space-around;
+    .item{
+      text-align: center;
+      font-size: 24rpx;
+      color: #312520;
+      padding: 35rpx 0 33rpx;
+      text.iconfont{
+        font-size: 56rpx;
+        color:#25515E;
+        display: block;
+      }
+      .num{
+        font-weight: 600;
+        font-size: 48rpx;
+        color: #FF8719;
+        margin-top: 12rpx;
+        line-height: 48rpx;
+        margin-bottom: 15rpx;
+      }
+      .level{
+        margin-top: 12rpx;
+        margin-bottom: 15rpx;
+        font-family: Rockwell;
+        font-size: 44rpx;
+        line-height: 48rpx;
+        color: #FF8719;
+      }
+    }
+  }
+  .people-list{
+    .item{
+      width: 100%;
+      display: flex;
+      align-items: center;
+      padding: 28rpx 21rpx 28rpx 10rpx;
+      text-align: left;
+      .rank{
+        position: relative;
+        top:inherit;
+        left:inherit;
+        margin-right: 25rpx;
+        width: 77rpx;
+      }
+      .info{
+        flex:1;
+      }
+      .level{
+        font-family: Rockwell;
+        font-size: 36rpx;
+        color: #FF8719;
+      }
+      image.avatar{
+        margin-bottom: 0;
+        margin-right: 51rpx;
+        background-color: #efefef;
+        border-radius: 50%;
+      }
+    }
+  }
+}
+</style>

+ 55 - 0
src/pages/dig/task/building.vue

@@ -0,0 +1,55 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_building.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-building-1"></text>
+        <view class="info">
+          <view class="title">建筑分布</view>
+          <view class="desc">村落内传统建筑分布情况</view>
+        </view>
+        <view class="btn" @click="goForm('distribution', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-building-2"></text>
+        <view class="info">
+          <view class="title">文物建筑</view>
+          <view class="desc">历史、艺术、科学价值</view>
+        </view>
+        <view class="btn" @click="goForm('building', 1, 'nature')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-building-3"></text>
+        <view class="info">
+          <view class="title">历史建筑</view>
+          <view class="desc">重大历史事件记录</view>
+        </view>
+        <view class="btn" @click="goForm('building', 2, 'nature')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-building-4"></text>
+        <view class="info">
+          <view class="title">重要传统建筑</view>
+          <view class="desc">重要传统建筑的信息</view>
+        </view>
+        <view class="btn" @click="goForm('building', 3, 'nature')">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>

+ 75 - 0
src/pages/dig/task/custom.vue

@@ -0,0 +1,75 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_custom.png"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-custom-1"></text>
+        <view class="info">
+          <view class="title">非物质文化遗产项目</view>
+          <view class="desc">维护文化多样性</view>
+        </view>
+        <view class="btn" @click="goForm('ich', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom-2"></text>
+        <view class="info">
+          <view class="title">节庆活动</view>
+          <view class="desc">欢庆与传承并重的文化盛宴</view>
+        </view>
+        <view class="btn" @click="goForm('folk_culture', 1, 'folk_culture_type')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom-3"></text>
+        <view class="info">
+          <view class="title">祭祀崇礼</view>
+          <view class="desc">对先贤与自然的崇高致敬</view>
+        </view>
+        <view class="btn" @click="goForm('folk_culture', 2, 'folk_culture_type')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom-4"></text>
+        <view class="info">
+          <view class="title">婚丧嫁娶</view>
+          <view class="desc">生命礼赞与文化传承的双重奏鸣</view>
+        </view>
+        <view class="btn" @click="goForm('folk_culture', 3, 'folk_culture_type')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom-5"></text>
+        <view class="info">
+          <view class="title">地方方言</view>
+          <view class="desc">历史沉淀的语言瑰宝</view>
+        </view>
+        <view class="btn" @click="goForm('folk_culture', 4, 'folk_culture_type')">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-custom-6"></text>
+        <view class="info">
+          <view class="title">特色文化</view>
+          <view class="desc">民族精神的鲜明烙印</view>
+        </view>
+        <view class="btn" @click="goForm('folk_culture', 5, 'folk_culture_type')">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>

+ 46 - 0
src/pages/dig/task/environment.vue

@@ -0,0 +1,46 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_environment.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-environment-1"></text>
+        <view class="info">
+          <view class="title">自然环境</view>
+          <view class="desc">村落建立与发展历程</view>
+        </view>
+        <view class="btn" @click="goForm('environment', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-environment-5"></text>
+        <view class="info">
+          <view class="title">文物古迹</view>
+          <view class="desc">重要历史文献资料</view>
+        </view>
+        <view class="btn" @click="goForm('relic', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-environment-6"></text>
+        <view class="info">
+          <view class="title">历史环境</view>
+          <view class="desc">村民口述历史记录</view>
+        </view>
+        <view class="btn" @click="goForm('element', 0)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>
+

+ 35 - 0
src/pages/dig/task/food.vue

@@ -0,0 +1,35 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_food.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-food-1"></text>
+        <view class="info">
+          <view class="title">农副产品</view>
+          <view class="desc">乡村繁荣的多元支柱</view>
+        </view>
+        <view class="btn" @click="goForm('food_product', 1)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-food-2"></text>
+        <view class="info">
+          <view class="title">特色美食</view>
+          <view class="desc">给味蕾探索带来无限惊喜</view>
+        </view>
+        <view class="btn" @click="goForm('food_product', 3)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>

+ 76 - 0
src/pages/dig/task/history.vue

@@ -0,0 +1,76 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_history.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-history-1"></text>
+        <view class="info">
+          <view class="title">建村历史</view>
+          <view class="desc">村落建立与发展历程</view>
+        </view>
+        <view class="btn" @click="goForm('cultural', 1)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history-2"></text>
+        <view class="info">
+          <view class="title">历史人物</view>
+          <view class="desc">重要历史人物事迹</view>
+        </view>
+        <view class="btn" @click="goForm('figure', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history-3"></text>
+        <view class="info">
+          <view class="title">历史事件</view>
+          <view class="desc">重大历史事件记录</view>
+        </view>
+        <view class="btn" @click="goForm('cultural', 2)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history-4"></text>
+        <view class="info">
+          <view class="title">掌故轶事</view>
+          <view class="desc">民间传说与历史故事</view>
+        </view>
+        <view class="btn" @click="goForm('story', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history-5"></text>
+        <view class="info">
+          <view class="title">历史文献</view>
+          <view class="desc">重要历史文献资料</view>
+        </view>
+        <view class="btn" @click="goForm('cultural', 3)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-history-6"></text>
+        <view class="info">
+          <view class="title">口述历史</view>
+          <view class="desc">村民口述历史记录</view>
+        </view>
+        <view class="btn" @click="goForm('cultural', 4)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>
+

+ 45 - 0
src/pages/dig/task/mine.vue

@@ -0,0 +1,45 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_mine.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-mine-1"></text>
+        <view class="info">
+          <view class="title">商业集市</view>
+          <view class="desc">文化交流的开放窗口</view>
+        </view>
+        <view class="btn" @click="goForm('food_product', 4)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-mine-2"></text>
+        <view class="info">
+          <view class="title">服装服饰</view>
+          <view class="desc">艺术与功能交织的时尚篇章</view>
+        </view>
+        <view class="btn" @click="goForm('food_product', 5)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-mine-2"></text>
+        <view class="info">
+          <view class="title">运输工具</view>
+          <view class="desc">历史的运输工具</view>
+        </view>
+        <view class="btn" @click="goForm('food_product', 6)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>

+ 69 - 0
src/pages/dig/task/summary.vue

@@ -0,0 +1,69 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_summary.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-summary-1"></text>
+        <view class="info">
+          <view class="title">行政区划</view>
+          <view class="desc">村落行政区域划分及变迁</view>
+        </view>
+        <view class="btn" @click="goForm(1)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-summary-5"></text>
+        <view class="info">
+          <view class="title">村落综述</view>
+          <view class="desc">村落整体概况介绍</view>
+        </view>
+        <view class="btn" @click="goForm(5)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-summary-2"></text>
+        <view class="info">
+          <view class="title">地理信息</view>
+          <view class="desc">地理位置和自然环境特征</view>
+        </view>
+        <view class="btn" @click="goForm(2)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-summary-3"></text>
+        <view class="info">
+          <view class="title">建设与保护</view>
+          <view class="desc">村落发展与文化遗产保护</view>
+        </view>
+        <view class="btn" @click="goForm(3)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-summary-4"></text>
+        <view class="info">
+          <view class="title">人口与经济</view>
+          <view class="desc">人口与经济情况</view>
+        </view>
+        <view class="btn" @click="goForm(4)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const t = useTaskEntryForm();
+
+function goForm(subId: number) {
+  t.goForm('overview', subId, undefined, 'common');
+}
+</script>

+ 61 - 0
src/pages/dig/task/trip.vue

@@ -0,0 +1,61 @@
+<template>
+  <view class="main">
+    <view class="img-banner">
+      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_dig_trip.jpg"></image>
+    </view>
+    <view class="task-list">
+      <view class="item">
+        <text class="iconfont icon-task-trip-3"></text>
+        <view class="info">
+          <view class="title">旅游导览</view>
+        </view>
+        <view class="btn" @click="goForm('travel_guide', 0)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-trip-1"></text>
+        <view class="info">
+          <view class="title">游览路线</view>
+        </view>
+        <view class="btn" @click="goForm('route', 1)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-trip-2"></text>
+        <view class="info">
+          <view class="title">活动时间</view>
+        </view>
+        <view class="btn" @click="goForm('route', 2)">
+          填写
+        </view>
+      </view>
+      <view class="item">
+        <text class="iconfont icon-task-trip-4"></text>
+        <view class="info">
+          <view class="title">路线特色</view>
+        </view>
+        <view class="btn" @click="goForm('route', 3)">
+          填写
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useTaskEntryForm } from '../composeable/TaskEntryForm';
+
+const { goForm } = useTaskEntryForm();
+</script>
+
+<style lang="scss" scoped>
+.task-list {
+  .item{
+    .title{
+      margin-bottom: 0;
+    }
+  }
+}
+</style>

+ 550 - 0
src/pages/index.vue

@@ -0,0 +1,550 @@
+<template>
+  <view style="padding-bottom: 160rpx;">
+    <view class="top-slogan">
+      <view class="slogan">
+        <view class="h">翔安文化挖掘传承平台</view>
+        <view class="sub">
+          <text class="iconfont icon-chat"></text>香承文脉,两两相传,与你同行 ~~
+        </view>
+      </view>
+      <view class="search">
+        <uni-search-bar 
+          radius="100" 
+          bgColor="#fff" 
+          placeholder="搜一搜 马上出发" 
+          clearButton="auto" 
+          cancelButton="none"
+          @confirm="search"/>
+      </view>
+    </view>
+    <view class="main first">
+      <view class="banner">
+        <swiper class="swiper right-indicator" circular :indicator-dots="true" :autoplay="true" :interval="2000" :duration="1000">
+          <swiper-item v-for="item in bannerData" :key="item.id">
+            <view class="item">
+              <image :src="item.image" mode="aspectFill"></image>
+            </view>
+          </swiper-item>
+        </swiper>
+      </view>
+      <view class="navigation">
+        <view class="item">
+          <view class="left">
+            <view class="title">文化底蕴</view>
+            <view class="title_en">Cultural deposits</view>
+            <image src="https://mn.wenlvti.net/app_static/xiangan/MainBoxIcon1.png" mode="aspectFill"></image>
+            <view class="arrow">
+              <text class="iconfont icon-arrow-right"></text>
+            </view>
+          </view>
+          <view class="right">
+            <view class="cat top" @click="navTo('/pages/culture/history/calendar')"><view><view>历史</view><view>沿革</view></view></view>
+            <view class="cat" @click="navTo('/pages/culture/index', { tab: 1 })"><view><view>文化</view><view>动态</view></view></view>
+            <view class="cat bottom" @click="navTo('/pages/inherit/village/list')">村史馆</view>
+          </view>
+        </view>
+        <view class="item ">
+          <view class="left blue">
+            <view class="title">文化传承</view>
+            <view class="title_en">Cultural Heritage</view>
+            <image src="https://mn.wenlvti.net/app_static/xiangan/MainBoxIcon2.png" mode="aspectFill"></image>
+            <view class="arrow">
+              <text class="iconfont icon-arrow-right"></text>
+            </view>
+          </view>
+          <view class="right">
+            <view class="cat top" @click="navTo('/pages/inherit/intangible/index')">非遗</view>
+            <view class="cat" @click="navTo('/pages/inherit/artifact/index')">文物</view>
+            <view class="cat bottom" @click="navTo('/pages/culture/activity/calendar')"><view><view>文化</view><view>活动</view></view></view>
+          </view>
+        </view>
+        <view class="item">
+          <view class="left blue">
+            <view class="title">文化守护</view>
+            <view class="title_en">Guarding </view>
+            <image src="https://mn.wenlvti.net/app_static/xiangan/MainBoxIcon4.png" mode="aspectFill"></image>
+            <view class="arrow">
+              <text class="iconfont icon-arrow-right"></text>
+            </view>
+          </view> 
+          <view class="right">
+            <view class="cat top" @click="goWewuGuanjia"><view><view>文物</view><view>管家</view></view></view>
+            <view class="cat" @click="navTo('article/xiangjin/index')"><view><view>翔金</view><view>情缘</view></view></view>
+            <view class="cat bottom" @click="navTo('dig/index')"><view><view>共建</view><view>村史</view></view></view>
+          </view>
+        </view>
+        <view class="item ">
+          <view class="left">
+            <view class="title">文旅融合</view>
+            <view class="title_en">Cultural Tourism</view>
+            <image src="https://mn.wenlvti.net/app_static/xiangan/MainBoxIcon3.png" mode="aspectFill"></image>
+            <view class="arrow">
+              <text class="iconfont icon-arrow-right"></text>
+            </view>
+          </view>
+          <view class="right">
+            <view class="cat top" @click="navTo('running/index')">文物跑</view>
+            <view class="cat" @click="navTo('article/food/index')"><view><view>翔安</view><view>百味</view></view></view>
+            <view class="cat bottom" @click="navTo('article/mountainsea/index')"><view><view>山海</view><view>田厝</view></view></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 说透透 -->
+      <view class="category">
+        <view class="name">说透透</view>
+        <view class="more" @click="goNews(NewsIndexContent.modelId, 308)">更多》</view>
+      </view>
+      <view class="audio-list">
+        <SimplePageContentLoader 
+          :loader="speakLoader" lazy
+          :showEmpty="speakLoader.content.value?.length == 0"
+        >
+          <view class="item" 
+            v-for="item in speakLoader.content.value"
+            :key="item.id"
+            @click="goNewsDetails(item.id)"
+          >
+            <image :src="item.image" mode="aspectFill"></image>
+            <view class="info">
+              <view class="name">{{ item.title }}</view>
+              <view class="place">{{ item.desc || item.district }}</view>
+            </view>
+            <view class="audio">
+              <view class="play-btn">
+                <text class="iconfont icon-play-b"></text>
+              </view>
+              <view>{{ item.duration }}</view>
+            </view>
+          </view>
+        </SimplePageContentLoader>
+      </view>
+
+      <!-- 走透透 -->
+      <view class="category">
+        <view class="name">走透透</view>
+        <view class="more" @click="goNews(NewsIndexContent.modelId, 309)">更多》</view>
+      </view>
+      <view class="complex-swiper sm">
+        <SimplePageContentLoader 
+          :loader="walkLoader" lazy
+          :showEmpty="walkLoader.content.value?.length == 0"
+        >
+          <swiper 
+            v-if="walkLoader.content.value?.length" 
+            class="swiper"
+            :autoplay="false"
+            :circular="false"
+            :duration="500"
+            :next-margin="'235rpx'"
+          >
+            <swiper-item 
+              v-for="item in walkLoader.content.value"
+              :key="item.id"
+              @click="goNewsDetails(item.id)"
+            >
+              <view class="item">
+                <image :src="item.image" mode="aspectFill"></image>
+                <view class="name">
+                  <text>{{ item.title }}</text>
+                </view>
+              </view>
+            </swiper-item>
+          </swiper>
+        </SimplePageContentLoader>
+      </view>
+
+      <!-- 看透透 -->
+      <view class="category">
+        <view class="name">看透透
+          <view class="category-tag">
+            <text class="triangle"></text>
+            <text>探秘非遗,领略时光雕琢的惊艳技艺</text>
+          </view>
+        </view>
+        <view class="more" @click="navTo('/pages/inherit/intangible/index')">更多》</view>
+      </view>
+      <SimplePageContentLoader :loader="viewsLoader" lazy>
+        <scroll-view scroll-x>
+          <view class="d-flex flex-row">
+            <view 
+              v-for="(item, i) in viewsLoader.content.value"
+              :key="i"
+              class="mr-2"
+              @click="navTo('/pages/inherit/intangible/details', { id: item.id })"
+            >
+              <image 
+                class="width-300 height-200 radius-base"
+                :src="item.image"
+                mode="aspectFill"
+              />
+            </view>
+          </view>
+        </scroll-view>
+      </SimplePageContentLoader>
+    </view>
+
+    <!-- 最新资讯 -->
+    <view class="main">
+      <!-- 最新资讯 -->
+      <view class="category">
+        <view class="name">最新资讯</view>
+        <view class="more" @click="navTo('culture/index', { tab: 1 })">更多》</view>
+      </view>
+      <view class="news-simple-list-with-stats">
+        <SimplePageContentLoader 
+          :loader="newsLoader" lazy 
+          :showEmpty="newsLoader.content.value?.length == 0"
+        >
+          <view
+            class="item" 
+            v-for="item in newsLoader.content.value"
+            :key="item.id"
+            @click="goNewsDetails(item.id)"
+          >
+            <image :src="item.image"></image>
+            <view class="info">
+              <view class="name ellipsis-2">{{ item.title }} | {{ item.desc }}</view>
+              <view class="extra">
+                <text class="iconfont icon-time"></text>
+                <text>{{ DateUtils.formatDate(item.publishAt, DateUtils.FormatStrings.YearChanese) }}</text>
+                <text class="iconfont icon-view"></text>
+                <text>{{ item.views }}</text>
+                <text class="iconfont icon-fav"></text>
+                <text>{{ item.likes }}</text>
+              </view>
+            </view>
+          </view>
+        </SimplePageContentLoader>
+      </view>
+      <!-- 精彩推荐 -->
+      <view class="category">
+        <view class="name">精彩推荐</view>
+        <view class="more"></view>
+      </view>
+      <SimplePageContentLoader :loader="recommendLoader" lazy>
+        <view class="d-flex flex-row justify-between flex-wrap">
+          <view 
+            v-for="(tab, k) in recommendLoader.content.value"
+            :key="k"
+            class="grid4-item position-relative mb-3"
+            @click="handleGoDetails(tab)"
+          >
+            <text 
+              class="tag bg-mask-white color-primary radius-l p-1 position-absolute size-s text-lines-1"
+            >
+              {{ tab.title }}
+            </text> 
+            <image
+              class="w-100 height-250 radius-base"
+              :src="tab.thumbnail || tab.image || ImagesUrls.defaultImage"
+              mode="aspectFill"
+            />
+          </view>
+        </view>
+      </SimplePageContentLoader>
+    </view>
+    <tabbar :current="0"></tabbar>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
+import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
+import { navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
+import CommonContent, { GetContentListParams, type GetContentListItem } from "@/api/CommonContent";
+import NewsIndexContent from "@/api/news/NewsIndexContent";
+import UniSearchBar from "../uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue";
+import Tabbar from '@/pages/component/tabbar.vue'
+import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
+import DateUtils from "@imengyu/imengyu-utils/dist/DateUtils";
+import ProjectsContent from "@/api/inheritor/ProjectsContent";
+import ImagesUrls from "@/common/config/ImagesUrls";
+import ProductsContent from "@/api/inheritor/ProductsContent";
+
+
+const newsLoader = useSimplePageContentLoader<GetContentListItem[]>(async (params) => {
+  const res = await NewsIndexContent.getContentList(new GetContentListParams().setSelfValues({
+  }), 1, 4);
+  return res.list;
+});
+const walkLoader = useSimplePageContentLoader<GetContentListItem[]>(async (params) => {
+  const res = await NewsIndexContent.getContentList(new GetContentListParams().setSelfValues({
+    mainBodyColumnId: 309,
+  }), 1, 4);
+  return res.list;
+});
+const speakLoader = useSimplePageContentLoader<GetContentListItem[]>(async (params) => {
+  const res = await NewsIndexContent.getContentList(new GetContentListParams().setSelfValues({
+    mainBodyColumnId: 308,
+  }), 1, 4);
+  return res.list;
+});
+const viewsLoader = useSimplePageContentLoader<GetContentListItem[]>(async (params) => {
+  const res = await ProjectsContent.getContentList(new GetContentListParams().setSelfValues({
+  }), 1, 4);
+  return res.list;
+});
+const recommendLoader = useSimplePageContentLoader<GetContentListItem[]>(async (params) => {
+  const list = [];
+  list.push(...(await ProjectsContent.getContentList(new GetContentListParams(), 1, 6)).list.map((p) => {
+    p.itemType = 'intangible';
+    return p;
+  }));
+  list.push(...(await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(1)
+  , 1, 6)).list.map((p) => {
+    p.itemType = 'artifact';
+    return p;
+  }));
+  list.push(...(await ProductsContent.getContentList(new GetContentListParams(), 1, 6)).list.map((p) => {
+    p.itemType = 'intangible';
+    return p;
+  }));
+  return list;
+});
+
+const bannerData = [
+  {
+    id: 1,
+    image: 'https://mn.wenlvti.net/app_static/xiangan/index_banner_1.jpg',
+  },
+  {
+    id: 2,
+    image: 'https://mn.wenlvti.net/app_static/xiangan/index_banner_1.jpg',
+  }
+];
+
+
+function goNews(modelId: number, mainBodyColumnId?: number) {
+  navTo('/pages/article/list', {
+    modelId,
+    mainBodyColumnId,
+  });
+}
+function goNewsDetails(id: number) {
+  navTo('/pages/article/details', { id });
+}
+function handleGoDetails(item: GetContentListItem) {
+  switch (item.itemType) {
+    case 'artifact': 
+      navTo('/pages/inherit/artifact/details', { id: item.id });
+      break;
+    case 'intangible': 
+      navTo('/pages/inherit/intangible/details', { id: item.id });
+      break;
+    default:
+      navTo('/pages/article/details', { id: item.id });
+      break;
+  }
+}
+function search() {
+
+}
+function goWewuGuanjia() {
+  uni.navigateToMiniProgram({
+    appId: 'wxf651ba4b0025640a',
+    path: 'pages/index/index',
+    extraData: {},
+  })
+}
+
+onShareTimeline(() => {
+  return {}; 
+})
+onShareAppMessage(() => {
+  return {}; 
+})
+</script>
+
+<style lang="scss" scoped>
+
+.grid4-item {
+  width: 320rpx;
+
+  .tag {
+    top: 2rpx; 
+    right: 2rpx;
+  }
+}
+.audio-list {
+  .item {
+    display: flex;
+    margin-bottom: 25rpx;
+
+    image {
+      width: 126rpx;
+      height: 126rpx;
+      display: block;
+      border-radius: 10rpx;
+      margin-right: 17rpx;
+    }
+
+    .info {
+      flex: 1;
+      .name{
+        font-weight: 800;
+        font-size: 30rpx;
+        color: #333333;
+        margin-bottom: 20rpx;
+        margin-top: 10rpx;
+      }
+      .place {
+        font-size: 24rpx;
+        color: #C66207;
+        background: rgb(249,236, 225);
+        padding:12rpx 20rpx;
+        display: inline-block;
+        border-radius: 10rpx;
+      }
+    }
+
+    .audio {
+      .play-btn {
+        width: 70rpx;
+        height: 70rpx;
+        border-radius: 50%;
+        background: rgb(232, 232, 232);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 10rpx;
+
+        text.iconfont {
+          font-size: 32rpx;
+        }
+      }
+    }
+  }
+}
+.navigation{
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  .item{
+    display: flex;
+    margin-bottom: 30rpx;
+    .left{
+      background: #FF8719;
+      width: 169rpx;
+      height: 196rpx;
+      border-radius: 18rpx 0rpx 0rpx 18rpx;
+      padding:48rpx 0 49rpx 31rpx;
+      margin-right: 6rpx;
+      position: relative;
+      &.blue{
+        background:#24515D;
+      }
+      image {
+        width: 110rpx;
+        height: 146rpx;
+        position: absolute;
+        bottom: 0rpx;
+        right: 0rpx;
+      }
+      .title{
+        font-weight: 800;
+        font-size: 28rpx;
+        color: #fff;
+      }
+      .title_en{
+        font-size: 16rpx;
+        color: #fff;
+      }
+      .arrow{
+        margin-top: 120rpx;
+        width: 35rpx;
+        height: 35rpx;
+        border-radius: 50%;
+        background: #fff;
+        text-align: center;
+        line-height: 35rpx;
+        text.iconfont{
+          font-size: 24rpx;
+        }
+      }
+    }
+    .right{
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      .cat{
+        width: 122rpx;
+        height: 92rpx;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border: 1px solid #B1C7CD;
+        word-break: break-all;
+        line-height: 30rpx;
+        &.top{
+          border-radius: 0rpx 18rpx 0rpx 0rpx;
+        }
+        &.bottom{
+          border-radius: 0rpx 0rpx 18rpx 0rpx;
+        }
+      }
+    }
+  }
+}
+.banner {
+  margin-top: 0;
+  .swiper {
+    border-radius: 0;
+    height: 306rpx;
+    .item{
+      height: 246rpx;
+    }
+  }
+}
+.top-slogan{
+  background: #FF8719;
+  height:430rpx;
+  .slogan{
+    padding-top: 46rpx;
+    color:#fff;
+    padding-left: 68rpx;
+    .h{
+      font-size: 48rpx;
+      font-weight: 600;
+      margin-bottom: 20rpx;
+    }
+    .sub{
+      display: flex;
+      align-items: center;
+      margin-left: -10rpx;
+    }
+    text.iconfont{
+      display: inline-block;
+      font-size: 46rpx;
+      margin-right: 4rpx;
+    }
+  }
+}
+.search{
+  border:none;
+  margin: 10rpx 20rpx 0;
+  ::v-deep .uni-searchbar__box{
+    border:none;
+  }
+}
+.main.first{
+  margin-top:-153rpx;
+  padding-bottom: 0;
+}
+.threeD{
+  top: 24rpx;
+  left: inherit;
+  bottom: inherit;
+  right: 24rpx;
+  width: 70rpx;
+  height: 70rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  text.iconfont{
+    font-size: 45rpx;
+  }
+}
+</style>

+ 59 - 0
src/pages/parts/Box2LineImageRightShadow.vue

@@ -0,0 +1,59 @@
+<template>
+  <view 
+    class="d-flex flex-row flex-grow-1 justify-between shadow-s 
+    radius-base mb-3 p-2 pt-3 pb-3 overflow-hidden
+    border-all-light-light-primary"
+    :style="{ 
+      height: 'calc(100% - 20rpx)',
+      width: 'calc(100% - 10rpx)',
+    }"
+    @click="$emit('click')"
+  >
+    <view class="d-flex flex-row w-100">
+      <image 
+        :class="[
+          wideImage ? 'width-250' : 'width-150', 
+          'height-150', 
+          'radius-base',
+          'flex-shrink-0'
+        ]"
+        :src="image"
+        mode="aspectFill"
+      />
+      <view class="d-flex flex-col ml-3 flex-one width-500">
+        <text :class="[
+          'color-primary size-base',
+          desc || title1 ? 'text-lines-1' : 'text-lines-2',
+        ]">{{ title }}</text>
+        <text class="size-s color-second text-lines-2 mt-2">{{ desc }}</text>
+        <RoundTags v-if="tags" :tags="tags" small />
+      </view>
+    </view>
+    <text class="color-primary-second-text size-ss">{{ right }}</text>
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+import RoundTags from './RoundTags.vue';
+
+defineProps({
+  title: String,
+  desc: String,
+  right: String,
+  image: String,
+
+  tags: {
+    type: Array as PropType<string[]>,
+    default: null
+  },
+  wideImage: {
+    type: Boolean,
+    default: false,
+  },
+  title1: {
+    type: Boolean,
+    default: false,
+  }
+})
+</script>

+ 145 - 0
src/pages/parts/Box2LineLargeImageUserShadow.vue

@@ -0,0 +1,145 @@
+<template>
+  <view 
+    :class="[
+      'position-relative grid4-item',
+      'd-flex flex-col shadow-l radius-l p-2 mb-2 overflow-hidden',
+      'border-all-light-light-primary',
+      classNames,
+      fixSize ? 'flex-shrink-0' : ' flex-grow-1',
+    ]"
+    
+    :style="{ 
+      height: 'calc(100% - 20rpx)',
+      width: fixSize ? undefined : 'calc(100% - 10rpx)',
+    }"
+    @click="$emit('click')"
+  >
+    <image 
+      v-if="image" 
+      class="w-100 height-300 radius-base" 
+      :src="image" 
+      mode="aspectFill" 
+    />
+    <image 
+      v-if="videoMark" 
+      class="width-60 mr-2 video-mark" 
+      :src="PlayVideo" mode="widthFix" 
+    />
+    <view v-if="userName" class="d-flex flex-row align-center mt-2">
+      <image class="width-60 mr-2" :src="userHead" mode="widthFix" />
+      <text class="size-s">{{ userName }}</text>
+    </view>
+    <text 
+      :class="[
+        `color-${titleColor}`,
+        title1 || desc ? 'text-lines-1' : 'text-lines-2',
+        'mt-2',
+      ]"
+    >
+      {{ title }}
+    </text>
+    <text v-if="badge" class="position-absolute color-primary-text size-s bg-light-primary radius-base p-1 radius-s text-lines-1 r-0 t-0 mr-3 mt-3">{{ badge }}</text>
+    <text v-if="desc" class="color-second text-lines-2 mt-2">{{ desc }}</text>
+    <view v-if="likes !== undefined && comment !== undefined" class="d-flex flex-row mt-2">
+      <image class="width-40 mr-2" :src="IconHeart" mode="widthFix" />
+      <text class="size-s mr-3">{{ likes }}</text>
+      <image class="width-40 mr-2" :src="IconChat" mode="widthFix" />
+      <text class="size-s">{{ comment }}</text>
+    </view>
+    <RoundTags v-if="tags" :tags="tags" small />
+    <view v-if="bottomTime" class="d-flex flex-row mt-2">
+      <image class="width-40 mr-2" :src="IconTime" mode="widthFix" />
+      <text class="size-s mr-3">{{ bottomTime }}</text>
+    </view>
+    <view v-if="bottomLocate" class="d-flex flex-row justify-between mt-2">
+      <view class="d-flex flex-row align-center">
+        <image class="width-40 mr-2" :src="IconLocation" mode="widthFix" />
+        <text class="size-s">{{ bottomLocate }}</text>
+      </view>
+      <view class="d-flex flex-row align-center">
+        <image class="width-40 mr-2" :src="IconStar" mode="widthFix" />
+        <text class="size-s">{{ bottomScore }}</text>
+      </view>
+    </view> 
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+import RoundTags from './RoundTags.vue';
+
+const IconHeart = 'https://mncdn.wenlvti.net/app_static/minnan/images/discover/IconHeart.png';
+const IconChat = 'https://mncdn.wenlvti.net/app_static/minnan/images/discover/IconChat.png';
+const IconLocation = 'https://mncdn.wenlvti.net/app_static/minnan/images/inhert/IconLocation.png';
+const IconTime = 'https://mncdn.wenlvti.net/app_static/minnan/images/inhert/IconTime.png';
+const IconStar = 'https://mncdn.wenlvti.net/app_static/minnan/images/inhert/IconStar.png';
+const PlayVideo = 'https://mncdn.wenlvti.net/app_static/minnan/images/inhert/PlayVideo.png';
+
+defineProps({
+  classNames: {
+    type: String,
+  },
+  title: {
+    type: String,
+  },
+  titleColor: {
+    type: String,
+    default: 'primary',
+  },
+  title1: {
+    type: Boolean,
+    default: false,
+  },
+  fixSize: {
+    type: Boolean,
+    default: false,
+  },
+  badge: {
+    type: String,
+    default: '',
+  },
+  tags: {
+    type: Array as PropType<string[]>,
+    default: [],
+  },
+  videoMark: {
+    type: Boolean,
+    default: false,
+  },
+  desc: {
+    type: String,
+  },
+  image: {
+    type: String,
+  },
+  likes: {
+    type: Number,
+  },
+  comment: {
+    type: Number,
+  },
+  userHead: {
+    type: String,
+  },
+  userName: {
+    type: String,
+  },
+  bottomLocate: {
+    type: String,
+  },
+  bottomScore: {
+    type: String,
+  },
+  bottomTime: {
+    type: String,
+  },
+})
+</script>
+
+<style scoped>
+.video-mark {
+  position: absolute;
+  left: calc(50% - 20rpx);
+  top: 145rpx
+}
+</style>

+ 9 - 0
src/pages/parts/ContentNote.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="d-flex flex-row justify-content-center align-items-center p-3">
+    <img class="visible-hidden mr-2 width-40 height-40 flex-shrink-0" src="https://mncdn.wenlvti.net/app_static/minnan/images/icon_info.svg" />
+    <span class="visible-hidden color-text-content-second size-s">
+      此平台为公益平台,部分信息来源于网络,如涉侵权,请联系我们删除
+      <br>联系邮箱:153168270@qq.com
+    </span>
+  </div>
+</template>

+ 59 - 0
src/pages/parts/HomeTitle.vue

@@ -0,0 +1,59 @@
+<template>
+  <view 
+    :class="[
+      'home-title',
+      showMore ? 'has-more' : '',
+      inWing ? 'wing-l in-wing' : '',
+    ]"
+  >
+    <text>{{ title }}</text>
+    <text v-if="showMore" class="more" @click="$emit('clickMore')">
+      查看全部
+      <text class="iconfont icon-arrow-right ml-2" />
+    </text>
+  </view>
+</template>
+
+<script setup lang="ts">
+defineProps({	
+  title: {
+    type: String,
+    default: '',
+  },
+  inWing: {
+    type: Boolean,
+    default: false,
+  },
+  showMore: {
+    type: Boolean,
+    default: false,
+  }
+})
+defineEmits(['clickMore'])
+</script>
+
+<style lang="scss">
+.home-title {
+  font-size: 32rpx;
+  margin: 40rpx 0 20rpx 0;
+  color: #432A04;
+  font-family: "SongtiSCBlack";
+
+  .more {
+    font-size: 28rpx;
+  }
+  text {
+    font-family: "SongtiSCBlack";
+  }
+
+  &.has-more {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: baseline;
+  }
+  &.in-wing {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 63 - 0
src/pages/parts/ImageGrid.vue

@@ -0,0 +1,63 @@
+<template>
+  <view class="w-100 d-flex flex-row flex-wrap" :style="{ gap: `${gap}rpx` }">
+    <image 
+      v-for="(v, k) in images"
+      :key="k"
+      :src="imagekey ? v[imagekey] : v" 
+      :style="{ 
+        width: `calc(${100 / rowCount}% - ${gap}rpx)`,
+        height: imageHeight,
+        borderRadius: '10rpx',
+      }"
+      mode="aspectFill"
+      @click="itemClick(v, k)"
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+const props = defineProps({	
+  rowCount : {
+    type: Number,
+    default: 3,
+  },
+  imagekey : {
+    type: String,
+    default: undefined,
+  },
+  imageHeight : {
+    type: String,
+    default: undefined,
+  },
+  gap: {
+    type: Number,
+    default: 10,
+  },
+  images: {
+    type: Object as PropType<any[]>,
+    default: null,
+  },
+  preview: {  
+    type: Boolean,
+    default: false,
+  }
+})
+
+const emit = defineEmits([	
+  "itemClick"	
+])
+
+function itemClick(item: any, index: number) {
+  if (props.preview) {
+    uni.previewImage({
+      urls: props.images.map((v: any) => (props.imagekey ? v[props.imagekey] : v) || v),
+      current: (props.imagekey ? item[props.imagekey] : item) || item,
+    })
+  } else {
+    emit('itemClick', item, index);
+  }
+}
+
+</script>

+ 46 - 0
src/pages/parts/ImageSwiper.vue

@@ -0,0 +1,46 @@
+<template>
+  <swiper 
+    class="image-swiper" 
+    circular
+    :indicator-dots="true"
+    :autoplay="true"
+    :interval="2000"
+    :duration="1000"
+  >
+    <swiper-item v-for="(item, key) in images" :key="key">
+      <view class="item">
+        <image
+          :src="item"
+          class="w-100 radius-base"
+          mode="aspectFill"
+          @click="onPreviewImage(key)"
+        />
+      </view>
+    </swiper-item>
+  </swiper>
+</template>
+
+<script setup lang="ts">
+import { useSwiperImagePreview } from '@/common/composeabe/SwiperImagePreview';
+import type { PropType } from 'vue';
+
+const props = defineProps({
+  images: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+  },
+})
+
+const { onPreviewImage } = useSwiperImagePreview(() => props.images || [])
+
+</script>
+
+<style lang="scss">
+.image-swiper {
+  height: 400rpx;
+
+  image {
+    border-radius: 20rpx;
+  }
+}
+</style>

+ 52 - 0
src/pages/parts/RoundTags.vue

@@ -0,0 +1,52 @@
+<template>
+  <view 
+    class="d-flex flex-row flex-wrap mt-2"
+  >
+    <view 
+      v-for="(tag, k) in tags"
+      :key="k" 
+      class="bg-place mr-2 mb-2"
+      :style="{
+        maxWidth: '160rpx',
+        overflow: 'hidden',
+        textOverflow: 'ellipsis',
+        whiteSpace: 'nowrap',
+      }"
+      :class="[
+        tag ? '' : 'd-none',
+        small ? 'radius-l p-2 pt-0 pb-1' : 'radius-ll p-25 pt-1',
+      ]"
+    >
+      <text class="color-text-content-second size-ss">{{ tag }}</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, type PropType } from 'vue';
+
+const props = defineProps({
+  tags: {
+    type: Array as PropType<string[]>,
+    default: null,
+  },
+  tags2: {
+    type: Array as PropType<string[]>,
+    default: null,
+  },
+  small: {
+    type: Boolean,
+    default: false, 
+  }
+})
+
+const tagss = computed(() => {
+  if (props.tags && props.tags.length > 0) {
+    return props.tags;
+  } else if (props.tags2) {
+    return props.tags2;
+  } else {
+    return [];
+  }
+})
+</script>

+ 100 - 0
src/pages/parts/StatsText.vue

@@ -0,0 +1,100 @@
+<template>
+  <view 
+    :class="[
+      'main-stats-text',
+      title ? '' : 'no-title'
+    ]"
+  >
+    <text class="title">{{ title }}</text>
+    <view class="stats">
+      <view
+        v-for="(item, i) in data"
+        :key="i"
+        class="item"
+        :style="{ width }"
+        @click="item.onClick"
+      >
+        <text class="number">{{ item.value }}</text>
+        <text class="sub-title">{{ item.title }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+defineProps({	
+  title: {
+    type: String,
+    default: ''
+  },
+  data : {
+    type: Object as PropType<{
+      title: string,
+      value: string,
+      onClick?: () => void
+    }[]>,
+    default: () => ([])
+  }	,
+  width: {
+    type: [Number, String],
+    default: ''
+  },
+})
+</script>
+
+<style lang="scss">
+
+$color-primary: #ff8719;
+$color-text: '#333';
+
+.main-stats-text {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+
+  .title {
+    font-size: 30rpx;
+    margin: 10rpx 0;
+    color: $color-primary;
+    text-align: center;
+  }
+  .stats {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-around;
+    flex-wrap: wrap;
+    color: $color-text;
+
+    .item {
+      display: flex;
+      flex-direction: column;
+      text-align: center;
+      margin: 10rpx 0;
+
+      .sub-title {
+        font-size: 24rpx;
+      }
+      .number {
+        font-size: 50rpx;
+        font-weight: bold;
+      }
+    }
+  }
+
+  &.no-title {
+    .stats .item {
+      width: calc(30% - 60rpx);
+      flex-direction: column-reverse;
+
+      .sub-title {
+        font-size: 30rpx;
+        color: $color-primary;
+        margin-bottom: 10rpx;
+      }
+    }
+  }
+}
+</style>

+ 151 - 0
src/pages/user/index.vue

@@ -0,0 +1,151 @@
+<template>
+  <view class="main" style="padding-bottom:50rpx;">
+    <view v-if="userInfo" class="user-info">
+      <image :src="userInfo.avatar" mode="aspectFill" class="avatar"></image>
+      <view class="info">
+        <text class="nickname">{{ userInfo.nickname }}</text>
+        <text class="extra"><text class="label">守护编号</text><text>{{ userInfo.id }}</text><text class="label point-label">积分</text><text>{{ userInfo.totalCheckins }}</text></text>
+      </view>
+      <text class="iconfont icon-arrow-right"></text>
+    </view>
+    <view v-else class="user-info" @click="navTo('/pages/user/login')">
+      <image :src="UserHead" mode="aspectFill" class="avatar"></image>
+      <view class="info">
+        <text class="nickname">点击登录</text>
+        <text class="extra"> 登录后您将获得更多权益</text>
+      </view>
+      <text class="iconfont icon-arrow-right"></text>
+    </view>
+    <!-- <view class="list">
+      <view class="entry">
+        <image src="https://mn.wenlvti.net/uploads/20250313/042236758da5aaed21c1010e5b9440ce.png" mode="aspectFill"></image><text class="label">我的好友</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+      <view class="entry">
+        <image src="https://mn.wenlvti.net/uploads/20250313/9fb29e8bdb66490034145c90f892773a.png" mode="aspectFill"></image><text class="label">邀请好友</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+      <view class="entry">
+        <image src="https://mn.wenlvti.net/uploads/20250313/1366973c061bf98594036e42c0344593.png" mode="aspectFill"></image><text class="label">积分日志</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+    </view> -->
+    <!-- <view class="list">
+      <view class="entry" @click="navTo('/pages/dig/index')">
+        <image src="https://mn.wenlvti.net/uploads/20250313/07f750b4cf4959654c40171fdae91c3a.png" mode="aspectFill"></image><text class="label">投稿</text><view class="btn">去投稿</view><text class="iconfont icon-arrow-right"></text>
+      </view>
+    </view> -->
+    <!-- <view class="list">
+      <view class="entry">
+        <image src="https://mn.wenlvti.net/uploads/20250313/66d4665b1da5075e60148312469b2630.png" mode="aspectFill"></image><text class="label">我的投稿</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+      <view class="entry">
+        <image src="https://mn.wenlvti.net/uploads/20250313/acd97ca7b3f7736942495c7aec1dd65b.png" mode="aspectFill"></image><text class="label">加入我们</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+    </view> -->
+    <view v-if="userInfo" class="list">
+      <view class="entry" @click="doLogout">
+        <image src="https://mn.wenlvti.net/uploads/20250313/cbc47d0b9cad7891e6154359952858c6.png" mode="aspectFill"></image><text class="label">退出登录</text><text class="iconfont icon-arrow-right"></text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { confirm } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
+import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import { useAuthStore } from '@/store/auth';
+import { computed } from 'vue';
+import UserHead from '@/static/images/home/UserHead.png';
+
+const authStore = useAuthStore();
+const userInfo = computed(() => authStore.userInfo);
+
+function doLogout() {
+  confirm({
+    content: '您确定要退出登录吗?',
+  }).then((res) => {
+    if (res)
+      authStore.logout();
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.list{
+  background: #FFFFFF;
+  box-shadow: 0rpx 3rpx 6rpx 0rpx rgba(125,125,125,0.21);
+  border-radius: 20rpx;
+  margin-bottom: 34rpx;
+  .entry{
+    border-bottom: 1rpx solid #dddddd;
+    font-size: 28rpx;
+    color: #333333;
+    display: flex;
+    align-items: center;
+    padding: 30rpx 0;
+    margin: 0 30rpx;
+    image{
+      width: 32rpx;
+      height: 32rpx;
+    }
+    text.label{
+      flex:1;
+      margin-left: 12rpx;
+    }
+    &:last-child{
+      border-bottom: none;
+    }
+    text.iconfont{
+      color:#AAAAAA;
+      font-size: 20rpx;
+    }
+  }
+}
+.user-info{
+  display: flex;
+  align-items: center;
+  padding: 24rpx 8rpx 60rpx 8rpx;
+  image.avatar{
+    width: 127rpx;
+    height: 127rpx;
+    border-radius: 50%;
+    margin-right: 24rpx;
+  }
+  .info{
+    color:#111111;
+    flex:1;
+    .nickname{
+      font-weight: bold;
+      display: block;
+      font-size: 36rpx;
+      color: #333333;
+      margin-bottom: 20rpx;
+   }
+    .extra{
+      font-size: 24rpx;
+      text{
+        color: #333333;
+        font-weight: 600;
+      }
+      text.label{
+        display: inline-block;
+        margin-right: 10rpx;
+        color:#666666;
+        &.point-label{
+          margin-left: 24rpx;
+        }
+      }
+    }
+  }
+}
+.btn{
+  width: 148rpx;
+  height: 54rpx;
+  background: linear-gradient(0deg, #299365, rgba(41, 147, 101, 0.8));
+  border-radius: 27rpx;
+  font-weight: 400;
+  font-size: 24rpx;
+  color: #FFFFFF;
+  line-height: 54rpx;
+  text-align: center;
+  margin-right: 27rpx;
+}
+</style>

+ 0 - 0
src/pages/user/login.vue


Some files were not shown because too many files changed in this diff