imengyu 3 周之前
當前提交
eb947370be
共有 100 個文件被更改,包括 66887 次插入0 次删除
  1. 23 0
      .gitignore
  2. 20 0
      index.html
  3. 11802 0
      package-lock.json
  4. 74 0
      package.json
  5. 10 0
      shims-uni.d.ts
  6. 20 0
      src/App.vue
  7. 304 0
      src/api/CommonContent.ts
  8. 12 0
      src/api/NotConfigue.ts
  9. 218 0
      src/api/RequestModules.ts
  10. 108 0
      src/api/auth/UserApi.ts
  11. 10 0
      src/api/inheritor/ActivityContent.ts
  12. 10 0
      src/api/inheritor/InheritorContent.ts
  13. 19 0
      src/api/inheritor/MoveableContent.ts
  14. 10 0
      src/api/inheritor/ProductsContent.ts
  15. 10 0
      src/api/inheritor/ProjectsContent.ts
  16. 20 0
      src/api/inheritor/UnmoveableContent.ts
  17. 131 0
      src/api/inhert/VillageApi.ts
  18. 222 0
      src/api/inhert/VillageInfoApi.ts
  19. 10 0
      src/api/news/NewsIndexContent.ts
  20. 234 0
      src/api/running/RunningApi.ts
  21. 2 0
      src/changelog.md
  22. 963 0
      src/common/common.scss
  23. 27 0
      src/common/components/ImageWrapper.vue
  24. 27 0
      src/common/components/RequireLogin.vue
  25. 64 0
      src/common/components/SimpleDropDownPicker.vue
  26. 105 0
      src/common/components/SimplePageContentLoader.vue
  27. 27 0
      src/common/components/SimplePageListLoader.vue
  28. 90 0
      src/common/components/form/SimpleDynamicFormCate.vue
  29. 84 0
      src/common/components/form/SimpleDynamicFormCateInner.vue
  30. 223 0
      src/common/components/form/SimpleDynamicFormControl.vue
  31. 152 0
      src/common/components/form/SimpleDynamicFormUni.vue
  32. 29 0
      src/common/components/form/components/CityPicker.vue
  33. 45 0
      src/common/components/form/components/DynamicCheckbox.vue
  34. 45 0
      src/common/components/form/components/DynamicSelect.vue
  35. 38 0
      src/common/components/form/components/LonlatPicker.vue
  36. 50 0
      src/common/components/form/components/RichTextEditor.vue
  37. 45055 0
      src/common/components/form/data/city-data.json
  38. 42 0
      src/common/components/form/form/Form.vue
  39. 14 0
      src/common/components/form/form/FormItem.vue
  40. 159 0
      src/common/components/form/index.ts
  41. 112 0
      src/common/components/tabs/tabbar.vue
  42. 831 0
      src/common/components/y-video-slide/y-video-slide.vue
  43. 17 0
      src/common/composeabe/ErrorDisplay.ts
  44. 27 0
      src/common/composeabe/LoadQuerys.ts
  45. 9 0
      src/common/composeabe/LoaderCommon.ts
  46. 26 0
      src/common/composeabe/RequireLogin.ts
  47. 53 0
      src/common/composeabe/SimpleDataLoader.ts
  48. 97 0
      src/common/composeabe/SimpleLocalDataStorage.ts
  49. 40 0
      src/common/composeabe/SimplePageContentLoader.ts
  50. 70 0
      src/common/composeabe/SimplePageListLoader.ts
  51. 9 0
      src/common/config/ApiCofig.ts
  52. 16 0
      src/common/config/AppCofig.ts
  53. 353 0
      src/common/font.scss
  54. 4 0
      src/common/font_num.scss
  55. 61 0
      src/common/request/core/RequestApiConfig.ts
  56. 170 0
      src/common/request/core/RequestApiResult.ts
  57. 473 0
      src/common/request/core/RequestCore.ts
  58. 130 0
      src/common/request/core/RequestHandler.ts
  59. 33 0
      src/common/request/index.ts
  60. 52 0
      src/common/request/utils/AllType.ts
  61. 77 0
      src/common/request/utils/Utils.ts
  62. 15 0
      src/common/scss/define/border-radius.scss
  63. 37 0
      src/common/scss/define/colors.scss
  64. 32 0
      src/common/scss/define/margin-padding.scss
  65. 61 0
      src/common/scss/define/size.scss
  66. 42 0
      src/common/scss/define/wing-height.scss
  67. 69 0
      src/common/scss/global/base.scss
  68. 55 0
      src/common/scss/global/border.scss
  69. 17 0
      src/common/scss/global/color.scss
  70. 132 0
      src/common/scss/global/flex.scss
  71. 660 0
      src/common/scss/global/margin-padding.scss
  72. 51 0
      src/common/scss/global/radius.scss
  73. 15 0
      src/common/scss/global/shadow.scss
  74. 77 0
      src/common/scss/global/size.scss
  75. 165 0
      src/common/scss/global/text.scss
  76. 71 0
      src/common/scss/global/wing-space-height.scss
  77. 8 0
      src/common/style/commonParserStyle.ts
  78. 41 0
      src/common/utils/ArrayUtils.ts
  79. 104 0
      src/common/utils/CheckUtils.ts
  80. 294 0
      src/common/utils/CommonUtils.ts
  81. 88 0
      src/common/utils/ConvertRgeistry.ts
  82. 460 0
      src/common/utils/DateUtils.ts
  83. 78 0
      src/common/utils/DialogAction.ts
  84. 98 0
      src/common/utils/PageAction.ts
  85. 214 0
      src/common/utils/StringUtils.ts
  86. 8 0
      src/env.d.ts
  87. 19 0
      src/main.ts
  88. 95 0
      src/manifest.json
  89. 68 0
      src/package.json
  90. 120 0
      src/pages.json
  91. 92 0
      src/pages/article/details.vue
  92. 106 0
      src/pages/article/editor/editor.vue
  93. 23 0
      src/pages/article/editor/preview.vue
  94. 106 0
      src/pages/article/food/index.vue
  95. 95 0
      src/pages/article/list.vue
  96. 11 0
      src/pages/discover.vue
  97. 70 0
      src/pages/home.vue
  98. 11 0
      src/pages/inhert.vue
  99. 11 0
      src/pages/travel.vue
  100. 0 0
      src/pages/user/index.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>

文件差異過大導致無法顯示
+ 11802 - 0
package-lock.json


+ 74 - 0
package.json

@@ -0,0 +1,74 @@
+{
+  "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/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 {
+
+  }
+}

+ 20 - 0
src/App.vue

@@ -0,0 +1,20 @@
+<style lang="scss">
+  @import "@/uni_modules/uview-plus/index.scss";
+	@import "@/common/common.scss";
+	@import "@/common/scss/global/base.scss";
+</style>
+<script setup lang="ts">
+import { onLaunch } from '@dcloudio/uni-app'
+import { useAuthStore } from './store/auth'
+
+const authStore = useAuthStore();
+
+onLaunch(() => {
+  console.log('App Launch');
+  authStore.loadLoginState();
+})
+</script>
+
+<style>
+	/*每个页面公共css */
+</style>

+ 304 - 0
src/api/CommonContent.ts

@@ -0,0 +1,304 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from './RequestModules';
+import type { QueryParams } from '../common/request/utils/AllType';
+import ApiCofig from '@/common/config/ApiCofig';
+
+export class GetColumListParams extends DataModel<GetColumListParams> {
+  
+  public constructor() {
+    super(GetColumListParams);
+    this.setNameMapperCase('Camel', 'Snake');
+  }
+
+  /**
+   * 	主体栏目id
+   */
+  mainBodyColumnId: number|number[] = 0;
+  /**
+   * 标志:hot=热门,recommend=推荐,top=置顶
+   */
+  flag ?: 'hot'|'recommend'|'top';
+  /**
+   * 内容数量,默认4
+   */
+  size = 4;
+}
+export class GetContentListParams extends DataModel<GetContentListParams> {
+  
+  static TYPE_ARTICLE = 1;
+  static TYPE_AUDIO = 2;
+  static TYPE_VIDEO = 3;
+  static TYPE_ALBUM = 4;
+
+  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)),
+      },
+    }
+  }
+
+  /**
+   * 主体栏目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 },
+      content: { clientSide: 'string', serverSide: 'string' },
+      isGuest: { clientSide: 'boolean', serverSide: 'number' },
+      isLogin: { clientSide: 'boolean', serverSide: 'number' },
+      isComment: { clientSide: 'boolean', serverSide: 'number' },
+      isLike: { clientSide: 'boolean', serverSide: 'number' },
+      isCollect: { clientSide: 'boolean', serverSide: 'number' },
+      publishAt: { clientSide: 'date', serverSide: 'string' },
+      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
+      type: { clientSide: 'number', serverSide: 'number' },
+      associationMeList: [
+        { clientSide: 'map', serverSide: 'original'},
+        { clientSide: 'array', serverSide: 'original' },
+      ],
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+  }
+
+  id = 0;
+  from = '';
+  modelId = 0;
+  type = 0;
+  title = '';
+  region = 0;
+  image = '';
+  images = [];
+  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 CommonContentApi extends AppServerRequestModule<DataModel> {
+
+  constructor(mainBodyId = ApiCofig.mainBodyId, modelId = 0, debugName = 'CommonContent', mainBodyColumnId?: number) {
+    super();
+    this.modelId = modelId;
+    this.mainBodyId = mainBodyId;
+    this.mainBodyColumnId = mainBodyColumnId;
+    this.debugName = debugName;
+  }
+
+  protected mainBodyId: number;
+  protected mainBodyColumnId?: number;
+  protected modelId: number;
+  protected debugName: string;
+
+  /**
+   * 主体栏目列表
+   * @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: 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, querys?: QueryParams) {
+    return this.get('/content/content/getContentDetail', `${this.debugName} (${id}) 内容详情`, {
+      main_body_id: this.mainBodyId,
+      model_id: 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 = DataModel;
+  }
+}
+
+export default new CommonApi();

+ 218 - 0
src/api/RequestModules.ts

@@ -0,0 +1,218 @@
+
+/**
+ * 这里写的是业务相关的:
+ * * 请求数据处理函数。
+ * * 自定义请求模块。
+ * * 自定义错误报告处理函数。
+ */
+
+import { appendGetUrlParams, appendPostParams, isNullOrEmpty } from "@/common/request/utils/Utils";
+import AppCofig, { isDev } from "../common/config/AppCofig";
+import RequestApiConfig from "../common/request/core/RequestApiConfig";
+import { RequestApiError, RequestApiResult, type RequestApiErrorType } from "../common/request/core/RequestApiResult";
+import { RequestCoreInstance, RequestOptions, Response } from "../common/request/core/RequestCore";
+import { defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch } from "../common/request/core/RequestHandler";
+import type { DataModel, NewDataModel } from "@imengyu/js-request-transform";
+import type { KeyValue } from "@/common/utils/CommonUtils";
+import { useAuthStore } from "@/store/auth";
+import ApiCofig from "@/common/config/ApiCofig";
+
+/**
+ * 不报告错误的 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,追加到头;
+  const autoStore = useAuthStore();
+  if (isNullOrEmpty((req.header as KeyValue).token as string)) {
+    req.header['token'] = autoStore.token;
+    req.header['__token__'] = autoStore.token;
+  }
+  req.header['Accept'] = 'application/x.ttcxx.v1+json';
+  if (req.method == 'GET') {
+    //追加GET参数
+    url = appendGetUrlParams(url, 'main_body_id', ApiCofig.mainBodyId);
+  } else {
+    req.data = appendPostParams(req.data,'main_body_id', ApiCofig.mainBodyId);
+  }
+
+  return { newUrl: url, newReq: req };
+}
+//响应数据处理函数
+function responseDataHandler<T extends DataModel>(response: Response, 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 (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();
+    this.config.baseUrl = RequestApiConfig.getConfig().BaseUrl;
+    this.config.errCodes = []; //
+    this.config.requestInceptor = requestInceptor;
+    this.config.responseDataHandler = responseDataHandler;
+    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
+    this.config.reportError = reportError;
+  }
+}

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

@@ -0,0 +1,108 @@
+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._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,
+      ...data,
+    }, '登录', undefined, LoginResult)).data as LoginResult;
+  }
+  async loginWithMobile(data?: {
+    mobile: string,
+    password: string,
+  }) {
+    return (await this.post('/discover/User/login', {
+      appid: AppCofig.appId,
+      ...data,
+    }, '登录', 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();

+ 10 - 0
src/api/inheritor/ActivityContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class ActivityContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 16, "传承活动");
+  }
+}
+
+export default new ActivityContentApi();

+ 10 - 0
src/api/inheritor/InheritorContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class InheritorContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 7, "非遗保护名录-非遗传承人", 38);
+  }
+}
+
+export default new InheritorContentApi();

+ 19 - 0
src/api/inheritor/MoveableContent.ts

@@ -0,0 +1,19 @@
+import type { QueryParams } from '@/common/request/utils/AllType';
+import type { DataModel, NewDataModel } from '@imengyu/js-request-transform';
+import { CommonContentApi, GetColumContentList, GetColumListParams, GetContentListItem, GetContentListParams } from '../CommonContent';
+
+export class MoveableContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 1, "文物保护-可移动文物");
+  }
+
+  getContentList<T extends DataModel = GetContentListItem>(params: GetContentListParams, page: number, pageSize?: number, modelClassCreator?: NewDataModel, querys?: QueryParams): Promise<{ list: T[]; total: number; }> {
+    return super.getContentList(params, page, pageSize, modelClassCreator, { 
+      ...querys,
+      movable: 1
+    });
+  }
+}
+
+export default new MoveableContentApi();

+ 10 - 0
src/api/inheritor/ProductsContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class ProductsContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 2, "非遗保护名录-非遗产品", 295);
+  }
+}
+
+export default new ProductsContentApi();

+ 10 - 0
src/api/inheritor/ProjectsContent.ts

@@ -0,0 +1,10 @@
+import { CommonContentApi } from '../CommonContent';
+
+export class ProjectsContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 2, "非遗保护名录-非遗代表性项目", 258);
+  }
+}
+
+export default new ProjectsContentApi();

+ 20 - 0
src/api/inheritor/UnmoveableContent.ts

@@ -0,0 +1,20 @@
+import type { QueryParams } from '@/common/request/utils/AllType';
+import type { DataModel, NewDataModel } from '@imengyu/js-request-transform';
+import { CommonContentApi, GetColumContentList, GetColumListParams, GetContentListItem, GetContentListParams } from '../CommonContent';
+
+export class UnmoveableContentApi extends CommonContentApi {
+
+  constructor() {
+    super(undefined, 1, "文物保护-不可移动文物");
+  }
+
+
+  getContentList<T extends DataModel = GetContentListItem>(params: GetContentListParams, page: number, pageSize?: number, modelClassCreator?: NewDataModel, querys?: QueryParams): Promise<{ list: T[]; total: number; }> {
+    return super.getContentList(params, page, pageSize, modelClassCreator, { 
+      ...querys,
+      movable: 0
+    });
+  }
+}
+
+export default new UnmoveableContentApi();

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

@@ -0,0 +1,131 @@
+import { CONVERTER_ADD_DEFAULT, DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import ApiCofig from '@/common/config/ApiCofig';
+
+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;
+    };
+
+  }
+
+  id !: number;
+  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 = '';
+  villageName = '';
+  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 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, res.data2, `村落`, true))
+      .catch(e => { throw e });
+  }
+  async getCanClaimVallageList() {
+    return (this.get('/village/village/getList', '可认领村落列表', {
+    })) 
+      .then(res => transformArrayDataModel<VillageListItem>(VillageListItem, res.data2, `村落`, true))
+      .catch(e => { throw e });
+  }
+  async claimVallage(data: any) {
+    return (this.post('/village/village/addVillageClaim', {
+      ...data
+    }, '认领村落')) ;
+  }
+
+  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 });
+  }
+
+}
+
+export default new VillageApi();

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

@@ -0,0 +1,222 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import { transformSomeToArray } from '@/common/request/utils/Utils';
+
+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', clientSideRequired: true },
+      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 
+   */
+  async getCategoryList(
+    type?: number,
+    withself?: boolean,
+  ) {
+    return (this.get('/village/village/getVillageList', '获取分类列表', {
+      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 });
+  }
+
+  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();

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

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

+ 234 - 0
src/api/running/RunningApi.ts

@@ -0,0 +1,234 @@
+import { DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import ApiCofig from '@/common/config/ApiCofig';
+
+export class ActivityListItem extends DataModel<ActivityListItem> {
+  constructor() {
+    super(ActivityListItem, "活动列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time') || key === 'deadline')
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+
+  }
+
+  id = 0;
+  type = 0;
+  title = '';
+  organizers = null;
+  image = '';
+  desc = '';
+  name = '';
+  startTime : Date|null = null;
+  endTime : Date|null = null;
+  deadline : Date|null = null;
+  rewardTime : Date|null = null;
+  price = '';
+  status = '';
+  flagText = '';
+  typetext = '';
+  activityMethodsText = '';
+  statusText = '';
+}
+export class ActivityListDetail extends DataModel<ActivityListDetail> {
+  constructor() {
+    super(ActivityListDetail, "活动详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time') || key === 'deadline' || key === 'createdAt' || key === 'updatedAt')
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+  }
+
+  id = null as number|null;
+  mainBodyId = null as number|null;
+  type = '';
+  modelId = null as number|null;
+  contentIds = '';
+  adminId = null as number|null;
+  title = '';
+  tagIds = '';
+  organizers = '';
+  flag = '';
+  image = '';
+  images = [] as string[];
+  desc = '';
+  content = '';
+  startTime = null as Date|null;
+  endTime = null as Date|null;
+  deadline = null as Date|null;
+  rewardTime = null as Date|null;
+  limitNum = null as number|null;
+  applicantsNum = null as number|null;
+  price = '';
+  score = null as number|null;
+  activityMethods = '';
+  status = '';
+  weight = null as number|null;
+  createdAt = null as Date|null;
+  updatedAt = null as Date|null;
+  route = '';
+  flagText = '';
+  typeText = '';
+  activityMethodsText = '';
+  statusText = '';
+}
+export class GuideListItem extends DataModel<GuideListItem> {
+  constructor() {
+    super(GuideListItem, "宣讲员列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
+    }
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time') || key === 'deadline')
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+  }
+
+  id = '';
+  mainBodyUserId = null as number|null;
+  name = '';
+  image = '';
+  sex = null as number|null;
+  age = null as number|null;
+  mobile = '';
+  regionId = null as number|null;
+  address = '';
+  unitName = '';
+  intro = '';
+  status = '';
+  crTitle = '';
+  statusText = '';
+  sexText = '';
+  regionIdText = '';
+}
+export class GuideListDetail extends DataModel<GuideListDetail> {
+  constructor() {
+    super(GuideListDetail, "宣讲员详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      createdAt: { clientSide: 'date', serverSide: 'string' },
+    }
+  }
+
+  id = '';
+  mainBodyUserId = null as number|null;
+  name = '';
+  identityType = '';
+  createdAt = null as Date|null;
+  sex = null as number|null;
+  age = null as number|null;
+  mobile = '';
+  regionId = null as number|null;
+  intro = '';
+  statusText = '';
+  sexText = '';
+  regionIdText = '';
+}
+export class SignupParamsItem extends DataModel<SignupParamsItem> {
+  constructor() {
+    super(SignupParamsItem, "宣讲员详情");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      params: { clientSide: 'object', serverSide: 'string' },
+      setting: { clientSide: 'object', serverSide: 'string' },
+    }
+  }
+
+  id = null as number|null;
+  pid = null as number|null;
+  params = null as { [key: string]: any }|null;
+  setting = null as { [key: string]: any }|null;
+  name = '';
+  title = '';
+  type = '';
+  rule = '';
+  default = null as number|null;
+  status = '';
+  weight = null as number|null;
+}
+
+export class RunningApi extends AppServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+  async getActivityList(searchText?: string) {
+    return (this.get('/activity/activity/lists', '获取活动列表', {
+      keywords: searchText,
+      type: 3,
+    })) 
+      .then(res => transformArrayDataModel<ActivityListItem>(ActivityListItem, res.data2, `活动列表`, true))
+      .catch(e => { throw e });
+  }
+  async getActivityDetail(id: number) {
+    return (await this.get('/activity/activity/details', '获取活动详情', {
+      id,
+    }, ActivityListDetail)).data as ActivityListDetail;
+  }
+ 
+  async getGuideList(searchText?: string) {
+    return (this.get('/volunteer/volunteer/getXuanjiangList', '获取宣讲员列表', {
+      keywords: searchText,
+      type: 3,
+    })) 
+      .then(res => transformArrayDataModel<GuideListItem>(GuideListItem, res.data2, `宣讲员列表`, true))
+      .catch(e => { throw e });
+  }
+
+  async getScheduleList(activity_id: number) {
+    return (this.get('/activity/schedule/lists', '获取活动日程列表', {
+      activity_id,
+    })) 
+      .then(res => transformArrayDataModel<ActivityListDetail>(ActivityListDetail, res.data2, `活动日程列表`, true))
+      .catch(e => { throw e });
+  }
+
+  async getSignupParamsList(activity_id: number) {
+    return (this.get('/activity/signup_params/lists', '获取活动报名参数列表', {
+      activity_id,
+    })) 
+      .then(res => transformArrayDataModel<SignupParamsItem>(SignupParamsItem, res.data2, `活动报名参数列表`, true))
+      .catch(e => { throw e });
+  }
+
+  async getGuideDetail(id: string) {
+    return (await this.get('/volunteer/volunteer/getXuanjiangDetails', '获取宣讲员详情', {
+      volunteer_id: id,
+    }, GuideListDetail)).data as GuideListDetail;
+  }
+
+  async submitActivitySignupInfo(data: {
+    activity_id: number,
+    name: string,
+    mobile: string,
+    id_card: string,
+  }) {
+    await this.post('/volunteer/volunteer/getXuanjiangDetails', data, '活动报名');
+  }
+}
+
+export default new RunningApi();

+ 2 - 0
src/changelog.md

@@ -0,0 +1,2 @@
+## 1.0.0(2022-01-05)
+初始版本

+ 963 - 0
src/common/common.scss

@@ -0,0 +1,963 @@
+@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;
+    }
+  }
+}
+.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;
+      }
+    }
+  }
+}
+.tag{
+  top: 0;
+  left: 0;
+  background: #FF8719;
+  border-radius: 20rpx 0rpx 20rpx 0rpx;
+  padding:10rpx 20rpx;
+  position: absolute;
+  z-index: 99;
+  color: #fff;
+  &.blue{
+    background: #24515D;
+  }
+}
+.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;
+    }
+  }
+
+}
+.intro-block {
+  background: #FFFFFF;
+  border-radius: 10rpx;
+
+  .title {
+    font-weight: 600;
+    font-size: 36rpx;
+    color: #111111;
+    padding-top: 36rpx;
+    display: flex;
+    align-items: center;
+
+    .line {
+      background: #FF8719;
+      border-radius: 3rpx;
+      margin-right: 14rpx;
+      width: 6rpx;
+      height: 38rpx;
+    }
+  }
+
+  .desc {
+    font-size: 28rpx;
+    color: #000000;
+    line-height: 56rpx;
+    padding: 38rpx 20rpx 42rpx;
+    text-indent: 2em;
+    &.no-indent {
+      text-indent: 0;
+    }
+    view.entry{
+      display: flex;
+      margin-bottom: 30rpx;
+      view.label{
+        font-weight: 600;
+        font-size: 28rpx;
+        color: #000000;
+        margin-right: 10rpx;
+      }
+    }
+  }
+}
+::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;
+    .tag{
+
+    }
+    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;
+      .item{
+        width: 580rpx;
+        height: 360rpx;
+        transform: scale(0.8);
+        transition: transform 0.3s ease;
+        position: relative;
+        &.active {
+          transform: scale(1);
+        }
+      }
+    }
+  }
+  .swiper {
+    height: 300rpx;
+  }
+  .item {
+    width: 514rpx;
+    height: 300rpx;
+    position: relative;
+    border-radius: 20rpx;
+    overflow: hidden;
+    image {
+      width: 100%;
+      height: 100%;
+    }
+    .name {
+      position: absolute;
+      bottom: 34rpx;
+      right: 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;
+    }
+  }
+}

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

@@ -0,0 +1,27 @@
+<template>
+  <view class="image-wrapper">
+    <u-image 
+      :showLoading="true"
+      v-bind="$props"
+      :src="src || EmptyImage"
+    >
+      <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,
+  },
+})
+</script>

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

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

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

@@ -0,0 +1,64 @@
+<template>
+  <view 
+    class="simple-dropdown-box" 
+    @click="show=true"
+  >
+    {{ 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: '请选择',
+  },
+})
+
+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;
+}
+</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 ?? 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>

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

@@ -0,0 +1,223 @@
+<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>
+      <!-- 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';
+
+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 '@/common/utils/DialogAction';
+import type { FormDefine, FormDefineItem, FormExport } from '.';
+import SimpleDynamicFormCate from './SimpleDynamicFormCate.vue';
+import CommonUtils from '@/common/utils/CommonUtils';
+
+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 CommonUtils.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>

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

@@ -0,0 +1,29 @@
+<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 data from '../data/city-data.json'
+
+const props = defineProps({	
+  modelValue: { 
+    type: Array,
+    default: null 
+  },
+  useCode: {
+    type: Boolean,
+    default: false,
+  },
+})
+const emit = defineEmits(['update:modelValue'])
+
+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>

+ 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 '@/common/utils/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>

文件差異過大導致無法顯示
+ 45055 - 0
src/common/components/form/data/city-data.json


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

+ 112 - 0
src/common/components/tabs/tabbar.vue

@@ -0,0 +1,112 @@
+<template>
+  <view class="custom-tabbar">
+    <view class="row">
+      <view class="tabbar-item" @click="switchTab('/pages/home', 0)"  :class="{active: current === 0}">
+        <image :src="current === 0 ? '/static/images/tabs/icon_home_on.png' : '/static/images/tabs/icon_home_off.png'" mode="aspectFit"></image>
+        <text>首页</text>
+      </view>
+      <view class="tabbar-item" @click="switchTab('/pages/discover', 1)" :class="{active: current === 1}">
+        <image :src="current === 1 ? '/static/images/tabs/icon_discover_on.png' : '/static/images/tabs/icon_discover_off.png'" mode="aspectFit"></image>
+        <text>发现</text>
+      </view>
+      <view class="tabbar-item center" @click="switchTab('/pages/inhert', 2)" :class="{active: current === 2}">
+        <image :src="current === 2 ? '/static/images/tabs/icon_inhert_on.png' : '/static/images/tabs/icon_inhert_off.png'" mode="aspectFit"></image>
+        <text>传承</text>
+      </view>
+      <view class="tabbar-item" @click="switchTab('/pages/travel', 3)" :class="{active: current === 3}">
+        <image :src="current === 3 ? '/static/images/tabs/icon_shop_on.png' : '/static/images/tabs/icon_shop_off.png'" mode="aspectFit"></image>
+        <text>文旅</text>
+      </view>
+      <view class="tabbar-item" @click="switchTab('/pages/user/index', 4)" :class="{active: current === 4}">
+        <image :src="current === 4 ? '/static/images/tabs/icon_profile_on.png' : '/static/images/tabs/icon_profile_off.png'" mode="aspectFit"></image>
+        <text>我的</text>
+      </view>
+    </view>
+    <u-safe-bottom />
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    current: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+    }
+  },
+  methods: {
+    switchTab(path, index) {
+      if (this.current === index) return
+      uni.switchTab({
+        url: path
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.custom-tabbar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 999;
+  height: auto;
+  background-color: #ffffff;
+  box-shadow: 0 -2rpx 4rpx rgba(0, 0, 0, 0.1);
+
+  display: flex;
+  flex-direction: column;
+
+  .row {
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    padding-top: 20rpx;
+  }
+  .tabbar-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    &.center{
+      margin-top: -58rpx;
+      image{
+        width: 120rpx;
+        height: 120rpx;
+      }
+      text {
+        margin-top: 0rpx;
+        z-index: 99;
+      }
+      &.active {
+        text {
+          color: #d94a2f;
+        }
+      }
+    }
+    image {
+      width: 50rpx;
+      height: 50rpx;
+    }
+
+    text {
+      font-size: 24rpx;
+      color: #111111;
+      margin-top: 8rpx;
+    }
+
+    &.active {
+      text {
+        color: #d94a2f;
+      }
+    }
+  }
+}
+</style>

+ 831 - 0
src/common/components/y-video-slide/y-video-slide.vue

@@ -0,0 +1,831 @@
+<template>
+	<view class="widget-video" :style="{height:videoHeight}">
+		<!-- 下拉刷新 -->
+		<view v-if="marginTop>0" class="refresh-box">
+			松开刷新
+		</view>
+		<!-- 视频容器 -->
+		<view class="scroll-video-box" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd"
+			:style="{'marginTop':marginTop+'px','transition':transition}">
+			<view ref="videoItemBox" class="video-item-box" v-for="(item, index) in videoList" :key="index"
+				:style="{height:videoHeight}">
+				<video @click="$event => {pauseVideo($event, item)}" :id="item.id" class="fullscreen-video"
+					:src="item.videoUrl" @error="videoError(item,index)" :poster="item.posterUrl" :controls="false"
+					:show-progress="false" :show-fullscreen-btn="false" :show-play-btn="false" :loop="true"
+					:autoplay="index == 0" :show-center-play-btn="false">
+				</video>
+				<template v-for="(fabulous, fabulousIndx) in fabulousArr">
+					<i v-if="fabulous != null" @click="fabulousDbClick" class="iconfont iconxihuan fabulous-item"
+						:style="{'top':fabulous.top,'left':fabulous.left,'opacity':fabulous.opacity,'transform':fabulous.transform}"></i>
+				</template>
+
+				<i :class="['iconfont iconbofang btn-play',currentStatus == 'pause' && currentIndex == index ? 'show' : '']"
+					@click="pauseVideo"></i>
+				<view class="video-info">
+					<view class="atavar-box">
+						<image class="atavar-img" :src="item.userHead" mode="widthFix"></image>
+						<view class="add-follow-btn" @click="followFunc(index,item)">
+							<i
+								:class="['iconfont inline-block icon-follow', item.isFollow == 1 ? 'iconduihao' : 'iconjia1']"></i>
+						</view>
+					</view>
+					<view class="icon-box" @click="toggleFabulous(item, index)">
+						<i :class="['iconfont iconxihuan icon-btn', item.isFabulous ? 'color-red' : '']"></i>
+						<view class="count-text">{{item.fabulousCount}}</view>
+					</view>
+					<view class="icon-box" @click="commontAdd">
+						<i class="iconfont iconIMliaotian-shixin icon-btn"></i>
+						<view class="count-text">{{item.commentCount}}</view>
+					</view>
+					<view class="icon-box">
+						<i class="iconfont iconfenxiang icon-btn" @click="shareFunc(item)"></i>
+						<view class="count-text">{{item.shareCount}}</view>
+					</view>
+				</view>
+				<view class="video-title">
+					<view class="user-name">@{{item.userNick}}</view>
+					<view class="video-content">{{item.videoContent}}</view>
+				</view>
+			</view>
+			<view v-if="videoList.length == 0" class="empty-txt">当前没有可播放视频哦~</view>
+		</view>
+
+		<!-- 上拉加载 -->
+		<view v-if="videoList.length > 0" class="load-more-box">
+			加载更多
+		</view>
+		<!-- 评论弹窗 -->
+		<view :class="['commont-box',commontShow ? 'active' : '']">
+			<view class="commont-title">{{currentComment.count}}条评论</view>
+			<view class="commont-list">
+				<view v-for="(item, index) in currentComment.list" :key="index">
+					<view class="comment-panel">
+						<image class="first-user" :src="item.userHead" mode="widthFix"></image>
+						<view class="first-comment">
+							<view class="comment-name">{{item.userNick}}</view>
+							<view class="comment-content">{{item.content}}</view>
+							<view class="time-box">
+								<text class="inline-block">{{item.time}}</text>
+								<view class="inline-block reply-btn" @click="reply('first',item,index)">回复</view>
+							</view>
+						</view>
+						<view class="fabulous-box">
+							<i :class="['iconfont iconxihuan fabulous-btn', item.isFabulous == '1' ? 'color-red' : '']"
+								@click="commentFabulous('first',item,index)"></i>
+							<view class="fabulous-text">{{item.fabulousCount}}</view>
+						</view>
+					</view>
+					<view style="padding-left: 70rpx;">
+						<view class="comment-panel" v-for="(child, childIndex) in item.children">
+							<image class="first-user" :src="child.userHead" mode="widthFix"></image>
+							<view class="first-comment">
+								<view class="comment-name" v-if="child.replyTo == item.userNick">{{child.userNick}}
+								</view>
+								<view class="comment-name" v-else>
+									<span class="inline-block">{{child.userNick}}</span>
+									<span class="inline-block">
+										<i class="iconfont iconxiangyou1" style="font-size: 8px;"></i>
+									</span>
+									<span class="inline-block">{{child.replyTo}}</span>
+								</view>
+								<view class="comment-content">{{child.content}}</view>
+								<view class="time-box">
+									<text class="inline-block">{{child.time}}</text>
+									<view class="inline-block reply-btn"
+										@click="reply('second',child,index,childIndex)">回复</view>
+								</view>
+							</view>
+							<view class="fabulous-box">
+								<i :class="['iconfont iconxihuan fabulous-btn', child.isFabulous == '1' ? 'color-red' : '']"
+									@click="commentFabulous('second',child,index,childIndex)"></i>
+								<view class="fabulous-text">{{child.fabulousCount}}</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+			<!-- 评论输入框 -->
+			<input class="comment-input" @confirm="commentCommit" @blur="commentBlur" v-model="replyContent"
+				:focus="showInput" :placeholder="commentPlaceholder" type="text" />
+		</view>
+
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const dom = weex.requireModule('dom');
+	// #endif
+	export default {
+		props: {
+			// 视频容器高度
+			videoHeight: {
+				type: String,
+				default: '100vh'
+			},
+			// 列表数据
+			data: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			// 当前视频下标
+			videoIndex:{
+				type:[Number,String],
+				default:'0'
+			}
+		},
+		data() {
+			return {
+				// 视频列表数据
+				videoList: this.data,
+				// 视频实际高度,用于滚动计算
+				videoRealHeight: 0,
+				// 双击点赞记录
+				fabulousArr: [],
+				// 记录点击时间判断单击还是双击,单击暂停,双击点赞
+				clickTimer: null,
+				// 记录点击时间
+				clickTime: 0,
+				// 屏幕高度
+				windowHeight: uni.getSystemInfoSync().windowHeight || window.innerHeight,
+				// 动画效果
+				transition: 'none',
+				// 触摸开始值
+				startY: 0,
+				// 触摸移动值
+				moveY: 0,
+				// 滑动开始时间
+				startTime: 0,
+				// 向上滑动距离
+				marginTop: 0,
+				// 每次向上滑动的距离
+				fixMarginTop: 0,
+				// 滑动方向
+				moveDirection: '',
+				// 当前轮播的index
+				currentIndex: 0,
+				// 当前页的视频对象
+				currentVideo: null,
+				// 当前视频的评论
+				currentComment: [],
+				// 视频状态:播放/暂停
+				currentStatus: 'play',
+				// 是否显示评论
+				commontShow: false,
+				// 爱心动画双击
+				fabulousTime: 0,
+				fabulousTimer: null,
+				// 显示评论输入框
+				showInput: false,
+
+				// 评论提示
+				commentPlaceholder: '说点好听的~',
+				// 回复对象
+				replyType: 'video',
+				replyObj: {},
+				replyIndex: 0,
+				replyParentIndex: 0,
+				// 评论内容
+				replyContent: '',
+
+			};
+		},
+		watch: {
+			data: {
+				handler(n) {
+					this.videoList = n;
+				},
+				deep: true
+			}
+		},
+		// #ifdef APP-NVUE
+		onReady() {
+			dom.getComponentRect(this.$refs.videoItemBox, option => {
+				this.videoRealHeight = option.size.height;
+			});
+		},
+		// #endif
+		mounted() {
+			// 初始化视频容器计算高度
+			//#ifdef H5
+			this.videoRealHeight = this.$refs['videoItemBox'][0].$el.getBoundingClientRect().height;
+			//#endif
+			//#ifdef MP-WEIXIN
+			const query = uni.createSelectorQuery().in(this);
+			query.select('.video-item-box').boundingClientRect(data => {
+				this.videoRealHeight = data.height;
+			}).exec();
+			//#endif
+			// 初始化第一个视频播放器
+			this.currentVideo = uni.createVideoContext(this.videoList[0].id, this);
+			// 初始化评论
+			this.currentComment = this.videoList[0].commentObj;
+			// 定时清除动画元素
+			this.clearFabulousArr();
+		},
+		methods: {
+			// 视频出错
+			videoError(item, index) {
+				uni.showToast({
+					title: '视频出错了!',
+					icon: 'none'
+				})
+			},
+			// 触摸开始
+			touchStart(e) {
+				this.transition = 'none';
+				this.startY = e.touches[0].pageY;
+				this.startTime = new Date().getTime();
+			},
+			// 触摸滑动
+			touchMove(e) {
+				// 如果评论面板打开,禁止滑动
+				if (this.commontShow) {
+					return;
+				}
+				this.moveY = e.touches[0].pageY;
+				if (this.moveY > this.startY) {
+					// 返回上一个视频
+					let range = this.fixMarginTop + this.moveY - this.startY;
+					if (this.currentIndex == 0) {
+						this.marginTop = range > 50 ? 50 : range;
+					} else {
+						this.marginTop = range;
+					}
+					this.moveDirection = 'up';
+				} else {
+					if (this.videoList.length == 0) {
+						return;
+					}
+					// 查看下一个视频
+					let range = this.fixMarginTop - this.startY + this.moveY;
+					if (this.currentIndex == this.videoList.length - 1) {
+						// 最后一个视频的位置
+						let lastVideo = -this.currentIndex * this.videoRealHeight + -50;
+						this.marginTop = range < lastVideo ? lastVideo : range;
+					} else {
+						this.marginTop = range;
+					}
+					this.moveDirection = 'down';
+				}
+			},
+			// 触摸结束
+			touchEnd(e) {
+				this.transition = 'all .2s';
+				if (this.currentIndex == 0 && this.marginTop >= 45) {
+					// 下拉刷新
+					this.$emit('refresh');
+					this.marginTop = 0;
+					return;
+				}
+				if (this.currentIndex == this.videoList.length - 1 && this.marginTop < (this.fixMarginTop - 45)) {
+					this.marginTop = this.fixMarginTop;
+					// 加载更多
+					this.$emit('loadMore');
+					return;
+				}
+				let millisecond = new Date().getTime() - this.startTime;
+				let condition1 = this.moveY > 0 && Math.abs(this.moveY - this.startY) > 50 && millisecond < 500;
+				let condition2 = this.moveY > 0 && Math.abs(this.moveY - this.startY) > this.videoRealHeight / 3;
+				if (condition1 || condition2) {
+					if (this.moveDirection == 'up') {
+						// 返回上一个
+						if (this.currentIndex == 0) {
+							this.marginTop = 0;
+						} else {
+							this.marginTop = this.fixMarginTop + this.videoRealHeight;
+							this.currentIndex = this.currentIndex - 1;
+							this.$emit('update:videoIndex',this.currentIndex);
+							this.currentComment = this.videoList[this.currentIndex].commentObj;
+							this.videoPlayChange();
+						}
+					} else {
+						// 查看下一个
+						if (this.currentIndex == this.videoList.length - 1) {
+							this.marginTop = this.fixMarginTop;
+						} else {
+							this.marginTop = this.fixMarginTop - this.videoRealHeight;
+							this.currentIndex = this.currentIndex + 1;
+							this.$emit('update:videoIndex',this.currentIndex);
+							this.currentComment = this.videoList[this.currentIndex].commentObj;
+							this.videoPlayChange();
+						}
+					}
+				} else {
+					this.marginTop = this.fixMarginTop;
+				}
+				this.fixMarginTop = this.marginTop;
+				this.startY = 0;
+				this.moveY = 0;
+			},
+			// 滑动切换
+			videoPlayChange() {
+				this.stopOtherVideo();
+				let video = uni.createVideoContext(this.videoList[this.currentIndex].id, this);
+				this.currentVideo = video;
+				this.currentVideo.play();
+				this.currentStatus = 'play';
+			},
+			// 暂停其他视频
+			stopOtherVideo() {
+				this.videoList.map(v => {
+					let video = uni.createVideoContext(v.id, this);
+					video.seek(1);
+					video.pause();
+				})
+			},
+			// 暂停/播放视频/双击点赞事件
+			pauseVideo(e, item) {
+				// 视频对象为空,返回
+				if (!this.currentVideo) return;
+				if (this.commontShow) {
+					this.commontShow = false;
+					return;
+				}
+				clearTimeout(this.clickTimer);
+				let currentTime = new Date().getTime();
+				let timeRange = currentTime - this.clickTime;
+				if (timeRange < 300) {
+					// 双击事件
+					if (!item.isFabulous) {
+						this.$emit('fabulous', item, true);
+					}
+					this.dbClickAnimation(e);
+				} else {
+					// 单击事件
+					this.clickTimer = setTimeout(() => {
+						e.preventDefault();
+						e.stopPropagation();
+						if (this.currentStatus == 'play') {
+							this.currentVideo.pause();
+							this.currentStatus = 'pause';
+						} else {
+							this.currentVideo.play();
+							this.currentStatus = 'play';
+						}
+					}, 300);
+				}
+				this.clickTime = new Date().getTime();
+			},
+			// 已有爱心叠加双击事件
+			fabulousDbClick(e) {
+				clearTimeout(this.fabulousTimer);
+				let currentTime = new Date().getTime();
+				let timeRange = currentTime - this.fabulousTime;
+				if (timeRange < 300) {
+					// 双击事件
+					this.dbClickAnimation(e);
+				} else {
+
+				}
+				this.fabulousTime = new Date().getTime();
+			},
+			// 双击点赞动画
+			dbClickAnimation(e) {
+				let deg = Math.round(Math.random() * 40 + 5);
+				this.fabulousArr.push({
+					left: e.detail.x - 25 + 'px',
+					top: e.detail.y - 25 + 'px',
+					transform: 'rotate(' + (deg % 2 == 0 ? deg : -deg) + 'deg)',
+					createTime: new Date().getTime()
+				})
+
+				let index = this.fabulousArr.length > 0 ? this.fabulousArr.length - 1 : 0;
+				setTimeout(() => {
+					this.$set(this.fabulousArr, index, Object.assign(this.fabulousArr[index], {
+						opacity: 0,
+						transform: 'scale(3) ' + this.fabulousArr[index].transform
+					}))
+					setTimeout(() => {
+						this.fabulousArr.splice(index, 1, null);
+					}, 1000);
+				}, 500);
+
+				if (!this.videoList[this.currentIndex].isFabulous) {
+					this.$set(this.videoList, this.currentIndex, Object.assign(this.videoList[this.currentIndex], {
+						isFabulous: 1,
+						fabulousCount: this.videoList[this.currentIndex].isFabulous + 1
+					}))
+				}
+			},
+			// 定时清除点赞动画元素
+			clearFabulousArr() {
+				const indexArr = this.fabulousArr.map((v, i) => {
+					if (v.opacity == '0') {
+						return i;
+					}
+				});
+				indexArr.forEach(v => {
+					this.fabulousArr.splice(v, 1);
+				});
+			},
+			// 点赞切换
+			toggleFabulous(item, index) {
+				this.$emit('fabulous', item, item.isFabulous ? false : true);
+				this.$set(this.videoList, index, Object.assign(item, {
+					isFabulous: item.isFabulous ? 0 : 1,
+					fabulousCount: item.isFabulous ? item.fabulousCount - 1 : item.fabulousCount + 1,
+				}));
+			},
+			// 评论
+			commontAdd() {
+				this.commontShow = true;
+				this.replyType = 'video';
+				this.replyObj = this.currentVideo;
+			},
+			// 分享
+			shareFunc(item) {
+				this.$emit('share', item);
+			},
+			// 关注
+			followFunc(index, item) {
+				let flag = item.isFollow == 1 ? false : true;
+				if (flag) {
+					uni.showToast({
+						title: '关注成功',
+						icon: 'none'
+					})
+				} else {
+					uni.showToast({
+						title: '取消关注',
+						icon: 'none'
+					})
+				}
+				this.$emit('follow', item, flag);
+				this.$set(this.videoList, index, Object.assign(item, {
+					isFollow: item.isFollow == 1 ? 0 : 1
+				}));
+			},
+			// 评论点赞
+			commentFabulous(level, item, index, childIndex) {
+				const result = item.isFabulous == '1' ? '0' : '1';
+				item.isFabulous = result;
+				if (result == '1') {
+					item.fabulousCount = item.fabulousCount + 1;
+				} else {
+					item.fabulousCount = item.fabulousCount - 1;
+				}
+				if (level === 'first') {
+					this.currentComment.list[index] = item;
+				} else {
+					this.currentComment.list[index].children[childIndex] = item;
+				}
+				this.$emit('commentFabulous', item);
+			},
+			// 回复评论
+			reply(level, item, index, childIndex) {
+				this.showInput = true;
+				this.commentPlaceholder = `回复@${item.userNick}`;
+				this.replyIndex = childIndex;
+				this.replyType = level;
+				this.replyParentIndex = index;
+				this.replyObj = item;
+			},
+			// 提交评论
+			commentCommit() {
+				this.showInput = false;
+				if (this.replyType == 'video') {
+					this.currentComment.list.push({
+						userHead: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F5e%2F4e%2Ff0%2F5e4ef0e451852e0114d75eac14f60924.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1642669624&t=028d851350e18512dbf4bfe3a86cbfa4',
+						userNick: 'yjyyyyy',
+						content: this.replyContent,
+						fabulousCount: 0,
+					})
+				} else if (this.replyType == 'first') {
+					this.currentComment.list[this.replyParentIndex].children.push({
+						userHead: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F5e%2F4e%2Ff0%2F5e4ef0e451852e0114d75eac14f60924.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1642669624&t=028d851350e18512dbf4bfe3a86cbfa4',
+						userNick: 'yjyyyyy',
+						content: this.replyContent,
+						fabulousCount: 0,
+						replyTo: this.replyObj.userNick
+					});
+				} else {
+					this.currentComment.list[this.replyParentIndex].children.push({
+						userHead: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F5e%2F4e%2Ff0%2F5e4ef0e451852e0114d75eac14f60924.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1642669624&t=028d851350e18512dbf4bfe3a86cbfa4',
+						userNick: 'yjyyyyy',
+						content: this.replyContent,
+						fabulousCount: 0,
+						replyTo: this.replyObj.userNick
+					});
+				}
+				this.replyContent = '';
+			},
+			// 收起键盘
+			commentBlur() {
+				this.commentPlaceholder = `说点好听的~`;
+				this.showInput = false;
+				this.replyType = 'video';
+				this.replyObj = this.currentVideo;
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import '../../static/css/font.css';
+
+	.color-red {
+		color: #f73b3b;
+	}
+
+	.inline-block {
+		display: inline-block;
+		vertical-align: middle;
+	}
+
+	// 喜欢按钮动画
+	.iconxihuan,
+	.icon-follow {
+		transition: all .3s;
+	}
+
+	.iconxihuan:active,
+	.icon-follow:active {
+		transform: scale(1.5);
+	}
+
+	@keyframes double-fabulous {
+		from {
+			opacity: 1;
+			transform: scale(1);
+		}
+
+		to {
+			opacity: 0;
+			transform: scale(3);
+		}
+	}
+
+	// 双击点赞样式
+	.fabulous-item {
+		position: absolute;
+		z-index: 99999;
+		width: 100rpx;
+		height: 100rpx;
+		color: #f73b3b;
+		font-size: 100rpx;
+		// animation: double-fabulous 5s;
+		transform-origin: 50% 100%;
+		transition: opacity .8s, transform .8s;
+		opacity: 0.8;
+	}
+
+	// 评论框
+	.commont-box {
+		position: fixed;
+		z-index: -1;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 60%;
+		overflow-y: auto;
+		background-color: #222;
+		border-top-left-radius: 20rpx;
+		border-top-right-radius: 20rpx;
+		transition: all .3s;
+		transform: translateY(100%);
+		display: flex;
+		flex-direction: column;
+
+		&.active {
+			z-index: 9;
+			transform: translateY(0);
+		}
+
+		.commont-title {
+			text-align: center;
+			color: white;
+			font-size: 24rpx;
+			margin: 20rpx 0;
+			font-weight: bold;
+		}
+
+		.commont-list {
+			flex: 1;
+			overflow-y: auto;
+
+			.comment-panel {
+				margin: 30rpx 40rpx;
+				display: flex;
+
+				.first-user {
+					width: 50rpx;
+					height: 50rpx;
+					border-radius: 100%;
+				}
+
+				.first-comment {
+					flex: 1;
+					margin: 0rpx 20rpx;
+
+					.comment-name {
+						font-size: 24rpx;
+						font-weight: bold;
+						color: #999;
+					}
+
+					.comment-content {
+						color: white;
+						font-size: 26rpx;
+						margin-top: 10rpx;
+					}
+
+					.time-box {
+						margin-top: 10rpx;
+						font-size: 24rpx;
+						color: #999;
+
+						.reply-btn {
+							margin-left: 20rpx;
+						}
+					}
+				}
+
+				.fabulous-box {
+					text-align: center;
+					color: #666;
+
+					.fabulous-btn {
+						font-size: 36rpx;
+						transition: color .3s;
+					}
+
+					.fabulous-text {
+						font-size: 24rpx;
+						color: #999;
+					}
+				}
+			}
+		}
+
+		.comment-input {
+			display: block;
+			margin: 10px auto;
+			box-sizing: border-box;
+			width: 90%;
+			height: 70rpx;
+			padding: 0 20px;
+			border-radius: 60rpx;
+			background-color: #4a4a4a;
+			color: white;
+		}
+	}
+
+	// 全局容器
+	.widget-video {
+		width: 100%;
+		height: 100%;
+		overflow: hidden;
+		position: relative;
+
+		.scroll-video-box {
+			min-height: 100%;
+			position: relative;
+
+			.empty-txt {
+				position: absolute;
+				left: 50%;
+				top: 50%;
+				transform: translate(-50%, -50%);
+			}
+		}
+
+
+		.refresh-box {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 50px;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+
+		.load-more-box {
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			width: 100%;
+			height: 50px;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+
+		// 视频容器
+		.video-item-box {
+			position: relative;
+
+			.btn-play {
+				font-size: 80rpx;
+				color: white;
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transition: all .3s;
+				transform: translateX(-50%) translateY(-50%) scale(1.5);
+				z-index: 8;
+				opacity: 0;
+
+				&.show {
+					opacity: 0.5;
+					transform: translateX(-50%) translateY(-50%) scale(1);
+
+				}
+			}
+
+			.fullscreen-video {
+				width: 100%;
+				height: 100%;
+				position: relative;
+			}
+
+			.video-info {
+				width: 100rpx;
+				position: absolute;
+				right: 20rpx;
+				bottom: 100rpx;
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				align-items: center;
+
+				.atavar-box {
+					background-color: white;
+					padding: 6rpx;
+					border-radius: 100%;
+					width: 100rpx;
+					height: 100rpx;
+					margin-bottom: 60rpx;
+					position: relative;
+
+					.atavar-img {
+						width: 100%;
+						display: block;
+						border-radius: 100%;
+					}
+
+					.add-follow-btn {
+						width: 40rpx;
+						height: 40rpx;
+						padding: 4rpx;
+						border-radius: 100%;
+						color: white;
+						background-color: #f73b3b;
+						position: absolute;
+						bottom: -24rpx;
+						left: 50%;
+						transform: translateX(-50%);
+						font-weight: bold;
+						text-align: center;
+						line-height: 32rpx;
+
+						.iconjia1 {
+							font-size: 26rpx;
+						}
+					}
+				}
+
+				.icon-box {
+					width: 100rpx;
+					height: 100rpx;
+					margin-top: 30rpx;
+					display: flex;
+					align-items: center;
+					justify-content: space-around;
+					flex-direction: column;
+					color: white;
+
+					.icon-btn {
+						font-size: 60rpx;
+					}
+
+					.count-text {
+						font-size: 24rpx;
+					}
+				}
+
+			}
+
+			.video-title {
+				position: absolute;
+				left: 20rpx;
+				bottom: 20rpx;
+				width: calc(100% - 160rpx);
+				color: white;
+				z-index: 1;
+
+				.user-name {
+					font-weight: bold;
+				}
+
+				.video-content {
+					font-size: 24rpx;
+				}
+			}
+		}
+	}
+</style>

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

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

@@ -0,0 +1,27 @@
+import { onLoad } from "@dcloudio/uni-app";
+import { nextTick, ref, type Ref } from "vue";
+
+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,
+  }
+}

+ 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 "../utils/DialogAction";
+import { navTo } from "../utils/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('user/login');
+            return;
+          }
+        })
+      } else {
+        cb();
+      }
+    }
+  }
+}

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

@@ -0,0 +1,53 @@
+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,
+)  : ISimpleDataLoader<T, P>
+ {
+
+  const content = ref<T|null>(null) as Ref<T|null>;
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+
+  let lastParams: P | undefined;
+
+  async function loadData(params?: P) {
+    if (params)
+      lastParams = params;
+    loadStatus.value = 'loading';
+    try {
+      const res = (await loader(params ?? lastParams)) as T;
+      content.value = res;
+      loadStatus.value = 'finished';
+      loadError.value = '';
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      console.log(e);
+      
+    }
+  }
+
+  onMounted(() => {
+    if (loadWhenMounted) {
+      setTimeout(() => {
+        loadData();
+      }, (0.5 + Math.random()) * 1000);
+    }
+  })
+
+  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,
+  }
+}

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

@@ -0,0 +1,70 @@
+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>;
+}
+
+export function useSimplePageListLoader<T, P = any>(
+  pageSize: number, 
+  loader: (page: number, pageSize: number, params?: P) => Promise<T[]>)  : ISimplePageListLoader<T, P>
+{
+  
+  const loadStatus = ref<LoaderLoadType>('loading');
+  const loadError = ref('');
+  const page = 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;
+
+    try {
+      const res = (await loader(page.value, pageSize, lastParams)) as T[];
+      list.value = list.value.concat(res);
+
+      loadStatus.value = res.length > 0 ? 'finished' : 'nomore';
+      loadError.value = '';
+      loading = false;
+    } catch(e) {
+      loadError.value = '' + e;
+      loadStatus.value = 'error';
+      loading = false;
+    }
+  }
+
+  onPullDownRefresh(() => {
+    loadData(lastParams, true).then(() => {
+      uni.stopPullDownRefresh();
+    }).catch(() => {
+      uni.stopPullDownRefresh();
+    });
+  });
+  onReachBottom(() => {
+    if (loadStatus.value == 'nomore')
+      return;
+    loadData(lastParams, false);
+  });
+
+  return {
+    list,
+    page,
+    loadStatus,
+    loadError,
+    loadData,
+  }
+}

+ 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: 1,
+}

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

@@ -0,0 +1,16 @@
+
+/**
+ * 说明:应用静态配置
+ */
+export default {
+  version: '0.0.1',
+  appId: 'wx6da2b44a4ddf5248',
+}
+
+/**
+ * 图炫地图配置
+ */
+export function configAiMap() {
+}
+
+export const isDev = process.env.NODE_ENV === 'development';

文件差異過大導致無法顯示
+ 353 - 0
src/common/font.scss


文件差異過大導致無法顯示
+ 4 - 0
src/common/font_num.scss


+ 61 - 0
src/common/request/core/RequestApiConfig.ts

@@ -0,0 +1,61 @@
+/**
+ * 请求的默认配置
+ *
+ * 说明:
+ *  此处提供的是请求中的默认配置。
+ *
+ * Author: imengyu
+ * Date: 2022/03/25
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+import ApiCofig from "../../config/ApiCofig";
+import { isDev } from "../../config/AppCofig";
+import type { KeyValue } from "@imengyu/js-request-transform/dist/DataUtils";
+
+interface ApiConfigInterface {
+  /**
+   * 默认转换日期的格式
+   */
+  DataDateFormat: string,
+  /**
+  * 所有请求默认携带的header
+  */
+  DefaultHeader: KeyValue,
+  /**
+  * 是否在在控制台上打印出请求信息
+  */
+  EnableApiRequestLog: boolean,
+  /**
+  * 是否在每一个请求都在控制台上打印出休息数据
+  */
+  EnableApiDataLog: boolean,
+  /**
+   * 基础请求地址
+   */
+  BaseUrl: string;
+}
+
+const defaultConfig = {
+  BaseUrl: isDev ? ApiCofig.serverDev : ApiCofig.serverProd,
+  DataDateFormat: 'YYYY-MM-DD HH:mm:ss',
+  DefaultHeader: {
+		'content-type': 'application/json',
+  },
+  EnableApiRequestLog: true,
+  EnableApiDataLog: false,
+} as ApiConfigInterface;
+
+let config = defaultConfig;
+
+/**
+ * 请求中的默认配置
+ */
+const RequestApiConfig = {
+  getConfig() : ApiConfigInterface { return config; },
+  setConfig(newConfig: ApiConfigInterface): void { config = newConfig; },
+};
+
+export default RequestApiConfig;

+ 170 - 0
src/common/request/core/RequestApiResult.ts

@@ -0,0 +1,170 @@
+/**
+ * API 返回结构体定义
+ *
+ * 功能介绍:
+ *    这里定义了API返回数据的基本结构体,分为正常结果和错误结果。
+ *
+ * Author: imengyu
+ * Date: 2020/09/28
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+import { DataModel, type NewDataModel } from "@imengyu/js-request-transform";
+import type { KeyValue } from "@imengyu/js-request-transform/dist/DataUtils";
+
+/**
+ * API 的返回结构体
+ */
+export class RequestApiResult<T extends DataModel> {
+  public code = 0;
+  public message = '';
+  public data: T|KeyValue|null = null;
+  public data2: any = null;
+  public raw: any = null;
+
+  public constructor(c: NewDataModel|null, code? : number, message? : string, data?: Record<string, unknown>|null, rawData?: Record<string, unknown>|null) {
+    if (typeof code !== 'undefined')
+      this.code = code;
+    if (typeof message !== 'undefined')
+      this.message = message;
+
+    //转换数据
+    if (typeof data !== 'undefined' && c)
+      this.data = new c().fromServerSide(data as KeyValue) as T;//转换data
+    else if (typeof rawData !== 'undefined' && c)
+      this.data = new c().fromServerSide(rawData as KeyValue) as T;//如果data为空则转换rawData
+    else {
+      this.data = data as KeyValue as T; //原始数据
+      this.data2 = this.data;
+    }
+    if (typeof rawData !== 'undefined')
+      this.raw = rawData;
+    else
+      this.raw = this.data;
+  }
+
+  /**
+   * 使用另一个数据实例克隆当前结果
+   * @param model 另一个数据
+   * @returns
+   */
+  public cloneWithOtherModel<U extends DataModel>(model: U) : RequestApiResult<U> {
+    return new RequestApiResult(
+      null,
+      this.code,
+      this.message,
+      model.keyValue(),
+      this.raw
+    );
+  }
+  /**
+   * 转为纯JSON格式
+   * @returns
+   */
+  public keyValueData() : KeyValue {
+    return (this.data instanceof DataModel ? this.data?.keyValue() : this.data) || {};
+  }
+  /**
+   * 转为字符串表达形式
+   * @returns
+   */
+  public toString() : string {
+    return `${this.code} ${this.message} data: ${JSON.stringify(this.data)} raw: ` + JSON.stringify(this.raw);
+  }
+}
+
+/**
+ * 指示这个错误发生的类型
+ */
+export type RequestApiErrorType = 'networkError'|'statusError'|'serverError'|'businessError'|'scriptError'|'unknow';
+
+/**
+ * API 的错误信息
+ */
+export class RequestApiError {
+
+  /**
+   * 本次请求错误的 API 名字
+   */
+  public apiName = '';
+  /**
+   * 本次请求错误的 API URL
+   */
+  public apiUrl = '';
+  /**
+   * 指示这个错误发生的类型
+   * * networkError:网络连接错误
+   * * statusError:状态错误(返回了400-499错误状态码)
+   * * serverError:服务器错误(返回了500-599错误状态码)
+   * * businessError:业务错误(状态码200,但是自定义判断条件失败)
+   * * scriptError:脚本错误(通常是代码异常被catch)
+   */
+  public errorType : RequestApiErrorType = 'unknow';
+  /**
+   * 错误信息
+   */
+  public errorMessage: string;
+  /**
+   * code的错误信息
+   */
+  public errorCodeMessage: string;
+  /**
+   * 错误代号
+   */
+  public code = 0;
+  /**
+   * 本次请求的返回数据
+   */
+  public data: KeyValue|null = null;
+  /**
+   * 本次请求的原始返回数据
+   */
+  public rawData: KeyValue|null = null;
+  /**
+   * 本次请求的原始参数
+   */
+  public rawRequest: RequestInit|null = null;
+
+  public constructor(
+    errorType: RequestApiErrorType,
+    errorMessage = '',
+    errorCodeMessage = '',
+    code = 0,
+    data: KeyValue|null = null,
+    rawData: unknown|null = null,
+    rawRequest: RequestInit|null = null,
+    apiName = '',
+    apiUrl = ''
+  ) {
+    this.errorType = errorType;
+    this.errorMessage = errorMessage;
+    this.errorCodeMessage = errorCodeMessage;
+    this.code = code;
+    this.data = data;
+    this.apiName = apiName;
+    this.apiUrl = apiUrl;
+    this.rawData = rawData as KeyValue;
+    this.rawRequest = rawRequest as KeyValue;
+  }
+
+  /**
+   * 转为详情格式
+   * @returns
+   */
+  public toStringDetail() {
+    return `请求${this.apiName}错误 ${this.errorMessage} (${this.errorType}) ${this.code}(${this.errorCodeMessage})\n` +
+      `url: ${this.apiUrl}\n` +
+      `data: ${JSON.stringify(this.data)}\n` +
+      `rawData: ${JSON.stringify(this.rawData)}\n` +
+      `rawRequest: ${JSON.stringify(this.rawRequest)}\n`;
+  }
+  /**
+   * 转为字符串表达形式
+   * @returns
+   */
+  public toString(): string {
+    return this.errorMessage;
+  }
+}

+ 473 - 0
src/common/request/core/RequestCore.ts

@@ -0,0 +1,473 @@
+import RequestApiConfig from './RequestApiConfig';
+import { DataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { isNullOrEmpty, stringHashCode } from '../utils/Utils';
+import { RequestApiError, RequestApiResult } from './RequestApiResult';
+import { defaultResponseDataHandler, defaultResponseErrorHandler } from './RequestHandler';
+import type { HeaderType, QueryParams, TypeSaveable } from '../utils/AllType';
+import type { KeyValue } from '@imengyu/js-request-transform/dist/DataUtils';
+
+/**
+ * API 请求核心
+ *
+ * 功能介绍:
+ *    本类是对 fetch 的封装,提供了基本的请求功能。
+ *
+ * Author: imengyu
+ * Date: 2022/03/28
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+/**
+ * 请求配置体
+ */
+export interface RequestCoreConfig<T extends DataModel> {
+  /**
+   * 基础URL
+   */
+  baseUrl: string;
+  /**
+   * 错误代码字符串数据
+   */
+  errCodes: { [index: number]: string };
+  /**
+   * 默认携带header
+   */
+  defaultHeader: HeaderType,
+  /**
+   * 超时时间 ms
+   */
+  timeout: number,
+  /**
+   * 请求拦截
+   */
+  requestInceptor?: (url: string, req: RequestOptions) => { newUrl: string, newReq: RequestOptions };
+  /**
+   * 响应拦截
+   */
+  responseInceptor?: (response: Response) => Response;
+  /**
+   * 错误报告拦截。如果返回true,则不进行错误报告
+   */
+  responseErrReoprtInceptor?: (instance: RequestCoreInstance<T>, err: RequestApiError) => boolean;
+  /**
+   * 错误报告函数
+   */
+  reportError?: (instance: RequestCoreInstance<T>, err: RequestApiError|Error) => void;
+
+  /**
+   * 自定义数据处理函数
+   */
+  responseDataHandler?: (response: Response, req: RequestOptions, resultModelClass: NewDataModel|undefined, instance: RequestCoreInstance<T>, apiName: string|undefined) => Promise<RequestApiResult<T>>;
+  /**
+   * 自定义错误处理函数
+   */
+  responseErrorHandler?: (err: Error, instance: RequestCoreInstance<T>, apiName: string|undefined) => RequestApiError;
+  /**
+   * 类自定义创建函数
+   */
+  modelClassCreator: ModelClassCreatorDefine<T>|null;
+}
+
+type ModelClassCreatorDefine<T> = (new () => T);
+
+export interface RequestCacheConfig {
+  /**
+   * 缓存保存时间,毫秒。超过时间后再请求时会发请求
+   */
+  cacheTime: number,
+  /**
+   * 是否启用缓存
+   */
+  cacheEnable: boolean,
+}
+
+interface CacheStorage {
+  time: number,
+  data: TypeSaveable
+}
+
+export class RequestOptions {
+  /**
+   * 请求的参数
+   */
+  data?: string | object | ArrayBuffer;
+  /**
+  * 设置请求的 header,header 中不能设置 Referer。
+  */
+  header?: any;
+  /**
+  * 默认为 GET
+  * 可以是:OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE,CONNECT
+  */
+  method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
+  /**
+  * 超时时间
+  */
+  timeout?: number;
+  /**
+  * 如果设为json,会尝试对返回的数据做一次 JSON.parse
+  */
+  dataType?: string;
+  /**
+  * 设置响应的数据类型。合法值:text、arraybuffer
+  */
+  responseType?: string;
+  /**
+  * 验证 ssl 证书
+  */
+  sslVerify?: boolean;
+  /**
+  * 跨域请求时是否携带凭证
+  */
+  withCredentials?: boolean;
+  /**
+  * DNS解析时优先使用 ipv4
+  */
+  firstIpv4?: boolean;
+}
+export class Response {
+  public constructor(url: string, data: unknown, options: {
+    headers: Record<string, unknown>,
+    status: number,
+  }, errMsg: string) {
+    this.errMsg = errMsg;
+    this.data = data;
+    this.url = url;
+    this.status = options.status;
+    this.headers = options.headers;
+    this.ok = options.status >= 200 && options.status <= 399;
+    this.statusText = `Status: ${options.status}`;
+  }
+
+  headers: Record<string, unknown>;
+  ok: boolean;
+  status: number;
+  statusText: string;
+  errMsg: string;
+  url: string;
+  data: unknown;
+
+  json() : Promise<any> {
+    return new Promise<any>((resolve, reject) => {
+      if (typeof this.data === 'undefined' || isNullOrEmpty(this.data)) {
+        resolve({});
+        return;
+      }
+      if (typeof this.data === 'object') {
+        resolve(this.data);
+        return;
+      }
+      let data = null;
+      
+      if (typeof this.data === 'string') {
+        try {
+          data = JSON.parse(this.data);
+        } catch(e) {
+          console.log('json error: ' + e,  this.data);
+          
+          reject(e);
+        }
+      } else {
+        data = this.data;
+      }
+
+      resolve(data);
+    })
+  }
+}
+
+/**
+ * API 请求核心实例类,本类是对 fetch 的封装,提供了基本的请求功能。
+ */
+export class RequestCoreInstance<T extends DataModel> {
+
+  /**
+   * 当前请求实例的请求配置项
+   */
+  config : RequestCoreConfig<T> = {
+    baseUrl: '',
+    errCodes: {},
+    timeout: 10000,
+    defaultHeader: RequestApiConfig.getConfig().DefaultHeader as HeaderType,
+    modelClassCreator: null,
+    responseDataHandler: defaultResponseDataHandler,
+    responseErrorHandler: defaultResponseErrorHandler,
+  };
+
+  /**
+   * 检查是否需要报告错误
+   */
+  checkShouldReportError(err: RequestApiError) {
+    if (typeof this.config.responseErrReoprtInceptor === 'function')
+      return this.config.responseErrReoprtInceptor(this, err) !== true;
+    return true;
+  }
+  /**
+   * 报告错误
+   * @param err 错误
+   */
+  reportError(err: RequestApiError|Error) {
+    if (this.checkShouldReportError(err as RequestApiError)) {
+      if (typeof this.config.reportError === 'function')
+        this.config.reportError(this, err);
+    }
+  }
+  /**
+   * 在配置中查找错误代码的说明文字
+   * @param code 错误代码
+   * @returns 说明文字,如果找不到,返回 undefined
+   */
+  findErrCode(code: number) : string|undefined {
+    return this.config.errCodes[code];
+  }
+
+  /**
+   * 合并URL
+   */
+  makeUrl(url: string, querys?: QueryParams) {
+    let finalUrl = '';
+    if (url.indexOf('http') === 0)
+      finalUrl = url; //绝对地址
+    else
+      finalUrl = this.config.baseUrl + url;
+    //处理query
+    if (querys) {
+      let i = finalUrl.indexOf('?') > 0 ? 1 : 0;
+      for (const key in querys) {
+        if (typeof querys[key] === 'undefined' || querys[key] === null)
+          continue;
+        finalUrl += i === 0 ? '?' : '&';
+        if (typeof querys[key] === 'object')
+          finalUrl += `${key}=` + encodeURIComponent(JSON.stringify(querys[key]));
+        else
+          finalUrl += `${key}=` + '' + querys[key];
+        i++;
+      }
+    }
+    return finalUrl;
+  }
+  //合并默认Header参数
+  private mergerDefaultHeader(header: Record<string, unknown>) {
+    const myHeaders = {} as Record<string, unknown>;
+    for (const key in this.config.defaultHeader)
+      myHeaders[key] = this.config.defaultHeader[key];
+    if (header) {
+      for (const key in header) 
+        myHeaders[key] = header[key];
+    }
+    return myHeaders;
+  }
+  /**
+   * 合并两个Header参数
+   * @param header 合并目标
+   * @param newHeader 新的Header
+   * @returns 合并后的Header
+   */
+  mergerHeaders(header: Record<string, unknown>, newHeader: Record<string, unknown>) {
+    if (!newHeader)
+      return header;
+    if (!header)
+      return newHeader;
+    for (const key in newHeader)
+      header[key] = newHeader[key];
+    return header;
+  }
+
+  //检查缓存参数
+  private checkCacheTime(cache?: RequestCacheConfig) {
+    return cache && cache.cacheEnable && cache.cacheTime || 0;
+  }
+  //请求缓存处理
+  private solveCache(url: string, req: RequestOptions, cache: RequestCacheConfig|undefined, callback: (cacheTime: number, cacheKey: string, res: TypeSaveable) => void) {
+    const cacheTime = req.method === 'GET' ? this.checkCacheTime(cache) : 0;
+    let requestHash = '';
+    if (cacheTime > 0) {
+      requestHash = "RequestCache" + stringHashCode(url + req.method);
+      //获取数据
+      const c = localStorage.getItem(requestHash);
+      if (c) {
+        const cacheData = JSON.parse(c) as CacheStorage; 
+        if (!cacheData) {
+          callback(cacheTime, requestHash, null);
+          return;
+        }
+        //没有过期
+        if (cacheData.time < new Date().getTime()) {
+          callback(cacheTime, requestHash, cacheData.time);
+          return;
+        }
+        callback(cacheTime, requestHash, null);
+      }
+    } else {
+      callback(cacheTime, requestHash, null);
+    }
+  }
+
+  /**
+   * 通用的请求包装方法
+   * @param url 请求URL
+   * @param req 请求参数
+   * @param apiName 名称,用于日志和调试
+   * @returns 返回 Promise
+   */
+  request(url: string, req: RequestOptions,  apiName: string, modelClassCreator: NewDataModel|undefined, cache?: RequestCacheConfig) : Promise<RequestApiResult<T>> {
+    return new Promise<RequestApiResult<T>>((resolve, reject) => {
+      //附加请求头
+      req.header = this.mergerDefaultHeader(req.header);
+      
+      //拦截器
+      if (this.config.requestInceptor) {
+        const { newUrl, newReq } = this.config.requestInceptor(url, req);
+        url = newUrl;
+        req = newReq;
+      }
+
+      if (RequestApiConfig.getConfig().EnableApiRequestLog)
+        console.log(`[API Debugger] Q > ${apiName} [${req.method || 'GET'}] ` + url, req.data);
+
+      //缓存处理
+      this.solveCache(url, req, cache, (cacheTime, cacheKey, cacheRes) => {
+
+        //有缓存数据,则直接返回
+        if (cacheRes) {
+          if (RequestApiConfig.getConfig().EnableApiRequestLog)
+            console.log(`[API Debugger] C > ${apiName} (${cacheKey}/${cacheTime})`, ( RequestApiConfig.getConfig().EnableApiDataLog ? cacheRes.toString() : ''));
+          resolve(cacheRes as unknown as RequestApiResult<T>);
+          return;
+        }
+
+        //发送请求并且处理响应数据
+        this.requestAndResponse(url, req, apiName, modelClassCreator, (result) => {
+          //保存缓存
+          if (cacheTime > 0) {
+            localStorage.setItem(cacheKey, JSON.stringify({
+              time: new Date().getTime() + cacheTime,
+              data: result as unknown as TypeSaveable,
+            } as CacheStorage));
+          }
+        }).then((d) => {
+          resolve(d);
+        }).catch((e) => {
+          reject(e);
+        });
+      });
+    });
+  }
+
+  /*private startRequest(url: string, init?: RequestOptions, timeout = 10000): Promise<Response> {
+    // 创建 AbortController 实例
+    const controller = new AbortController();
+    const { signal } = controller;
+
+    // 设置超时逻辑
+    const timeoutId = setTimeout(() => {
+      controller.abort(); // 超时后取消请求
+    }, timeout);
+
+    // 发起 fetch 请求
+    const response = fetch(url, { ...init, signal });
+
+    // 请求完成后清除超时
+    response.finally(() => clearTimeout(timeoutId));
+    return response
+  }*/
+
+  private startUniappRequest(url: string, init?: RequestOptions, timeout = 10000): Promise<Response> {
+    return new Promise<Response>((resolve, reject) => {
+      uni.request({
+        url: url,
+        timeout: timeout,
+        ...init,
+        success(res) {
+          const response = new Response(url, res.data, {
+            headers: res.header,
+            status: res.statusCode,
+          }, 'success');
+          resolve(response);
+        },
+        fail(res) {
+          reject(res);
+        },
+      })
+    });
+  }
+  
+  //发送请求并且处理
+  private requestAndResponse(url: string, req: RequestOptions, apiName: string, resultModelClass: NewDataModel|undefined, saveCache?: (result: unknown) => void) {
+    return new Promise<RequestApiResult<T>>((resolve, reject) => {
+      //发起请求
+      this.startUniappRequest(url, req, this.config.timeout).then((res) => {
+        //响应拦截
+        if (this.config.responseInceptor)
+          res = this.config.responseInceptor(res);
+
+        if (this.config.responseDataHandler) {
+          //处理数据
+          this.config.responseDataHandler(res, req, resultModelClass, this, apiName).then((result) => {
+            //尝试保存缓存
+            saveCache && saveCache(result);
+            //处理数据
+            try {
+              if (RequestApiConfig.getConfig().EnableApiRequestLog)
+                console.log(`[API Debugger] R > ${apiName} (${res.status}/${result.code})`);
+              //返回
+              resolve(result);
+            } catch (e) {
+              //捕获处理代码的异常
+              console.error('[API Debugger] E > Catch exception in promise : ' + e + ((e as Error).stack ? ('\n' + (e as Error).stack) : ''));
+              reject(new RequestApiError('scriptError', '代码异常,请检查:' + e, '脚本异常', -1, null, e as unknown as KeyValue, req, apiName));
+            }
+          }).catch((e) => {
+            reject(e);
+          });
+        }
+        else
+          reject(new RequestApiError('scriptError', 'This RequestCoreInstance is not configured with responsedatahandler and cannot convert data! ', '脚本异常', -1, null, null, req, apiName));
+      }).catch((err) => {
+        reject(this.config.responseErrorHandler ? this.config.responseErrorHandler(err, this, apiName) : err);
+      });
+    });
+  }
+
+  /**
+   * GET 请求
+   * @param url 请求URL
+   * @param querys 请求URL参数
+   * @param cache 缓存参数
+   */
+  get(url: string, apiName: string, querys?: QueryParams, modelClassCreator?: NewDataModel, cache?: RequestCacheConfig, headers?: KeyValue) {
+    return this.request(this.makeUrl(url, querys), { method: 'GET', header: headers }, apiName, modelClassCreator, cache);
+  }
+  /**
+   * POST 请求
+   * @param url 请求URL
+   * @param data 请求Body参数
+   * @param querys 请求URL参数
+   * @param cache 缓存参数
+   */
+  post(url: string, data: KeyValue, apiName: string, querys?: QueryParams, modelClassCreator?: NewDataModel, cache?: RequestCacheConfig, headers?: KeyValue) {
+    return this.request(this.makeUrl(url, querys), { method: 'POST', data, header: headers }, apiName, modelClassCreator, cache);
+  }
+  /**
+   * PUT 请求
+   * @param url 请求URL
+   * @param data 请求Body参数
+   * @param querys 请求URL参数
+   * @param cache 缓存参数
+   */
+  put(url: string, data: KeyValue, apiName: string,querys?: QueryParams,  modelClassCreator?: NewDataModel, cache?: RequestCacheConfig, headers?: KeyValue) {
+    return this.request(this.makeUrl(url, querys), { method: 'PUT', data, header: headers }, apiName, modelClassCreator, cache);
+  }
+  /**
+   * DELETE 请求
+   * @param url 请求URL
+   * @param data 请求Body参数
+   * @param querys 请求URL参数
+   * @param cache 缓存参数
+   */
+  delete(url: string, data: KeyValue, apiName: string, querys?: QueryParams, modelClassCreator?: NewDataModel, cache?: RequestCacheConfig, headers?: KeyValue) {
+    return this.request(this.makeUrl(url, querys), { method: 'DELETE', data, header: headers }, apiName, modelClassCreator, cache);
+  }
+}

+ 130 - 0
src/common/request/core/RequestHandler.ts

@@ -0,0 +1,130 @@
+import ApiConfig from "./RequestApiConfig";
+import { DataModel, type NewDataModel } from "@imengyu/js-request-transform";
+import { RequestApiError, type RequestApiErrorType, RequestApiResult } from "./RequestApiResult";
+import { RequestCoreInstance, RequestOptions, Response } from "./RequestCore";
+
+/**
+ * 请求错误与数据处理函数
+ *
+ * 这里写的是请求中的 数据处理函数 与 错误默认处理函数。
+ *
+ * 业务相关的自定义数据处理函数,请单独在RequestModules中写明。
+ *
+ * Author: imengyu
+ * Date: 2022/03/28
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+//默认的请求数据处理函数
+export function defaultResponseDataHandler<T extends DataModel>(response: Response, 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) => {
+      //情况1,有返回数据
+      if (response.ok) {
+        if (ApiConfig.getConfig().EnableApiRequestLog)
+          console.log(`[API Debugger] Request [${method}] ` + response.url + ' success (' + response.status + ') ' + (ApiConfig.getConfig().EnableApiDataLog ? JSON.stringify(json) : ''));
+
+        //情况1-1,请求成功,状态码200-299
+        resolve(new RequestApiResult(resultModelClass ?? instance.config.modelClassCreator, response.status, json.message, json.data, json));
+      } else {
+        if (ApiConfig.getConfig().EnableApiRequestLog)
+          console.log(`[API Debugger] Request [${method}] ${response.url} Got error from server : ` + json.message + ' (' + json.code + ') ' + (ApiConfig.getConfig().EnableApiDataLog ? JSON.stringify(json) : ''));
+
+        //情况1-2,请求失败,状态码>299
+        const err = new RequestApiError('statusError', json.message, '状态码异常', json.code || response.status, json.data, json, req, apiName, response.url);
+
+        //错误报告
+        if (instance.checkShouldReportError(err))
+          instance.reportError(err);
+
+        reject(err);
+      }
+    }).catch((err) => {
+      //错误统一处理
+      defaultResponseDataHandlerCatch(method, req, response, null, err, apiName, response.url, reject, instance);
+    });
+  });
+}
+export function defaultResponseDataGetErrorInfo(response: Response, err: any) {
+  let errString = (response.status > 299) ? ('返回了状态码' + response.status + '。\n') : '';
+  let errType : RequestApiErrorType = 'statusError';
+  let errCodeStr = '状态码:' + response.status;
+  if (err instanceof Error && response.status < 299) {
+    errString = '代码错误: ' + err.message;
+    errType = 'scriptError';
+  } else {
+    if (('' + err).indexOf('JSON Parse error') >= 0)
+      errString += '处理JSON结构失败,可能后端没有返回正确的JSON格式。\n';
+
+    //情况2,没有返回数据
+    //错误状态码的处理
+    switch (response.status) {
+      case 400:
+        errCodeStr = '错误的请求';
+        errString += errCodeStr + ' \n[提示:请检查传入参数是否正确]';
+        errType = 'statusError';
+        break;
+      case 401:
+        errCodeStr = '未登录。可能登录已经过期,请重新登录';
+        errString += errCodeStr;
+        errType = 'statusError';
+        break;
+      case 405:
+        errCodeStr = 'HTTP方法不被允许';
+        errString += errCodeStr + ' \n[提示:这可能是调用接口是不正确造成的]';
+        errType = 'statusError';
+        break;
+      case 404:
+        errCodeStr = '返回404未找到';
+        errString += errCodeStr + ' \n[提示:后端检查下到底有没有提供这个API?]';
+        errType = 'statusError';
+        break;
+      case 500:
+        errCodeStr = '服务异常,请稍后重试';
+        errString += errCodeStr + ' \n[故障提示:这可能是后端服务出现了异常]';
+        errType = 'serverError';
+        break;
+      case 502:
+        errCodeStr = '无效网关,请反馈此错误';
+        errString += errCodeStr + ' \n[故障提示:请检查服务器与软件状态]';
+        errType = 'serverError';
+        break;
+      case 503:
+        errCodeStr = '服务暂时不可用';
+        errString += errCodeStr + ' \n[故障提示:请检查服务器状态]';
+        errType = 'serverError';
+        break;
+    }
+  }
+
+  return {errString, errType, errCodeStr};
+}
+//默认的请求数据处理函数
+export function defaultResponseDataHandlerCatch<T extends DataModel>(method: string, req: RequestOptions, response: Response, data: any, err: any, apiName: string|undefined, apiUrl: string, reject: (reason?: any) => void, instance: RequestCoreInstance<T>) {
+  if (ApiConfig.getConfig().EnableApiRequestLog) {
+    console.log(`[API Debugger] E > ${apiName} ` + err + ' status: ' + response.status);
+    if (err instanceof Error)
+      console.log(err.stack);
+  }
+
+  
+  const {errString, errType, errCodeStr} = defaultResponseDataGetErrorInfo(response, err);
+  const errObj = new RequestApiError(errType, errString, errCodeStr, response.status, null, data, req, apiName, apiUrl);
+
+  //错误报告
+  if (instance.checkShouldReportError(errObj))
+    instance.reportError(errObj);
+  reject(errObj);
+}
+
+//默认的请求错误处理函数
+export function defaultResponseErrorHandler(err: Error) : RequestApiError {
+  if (err instanceof Error)
+    console.error('[API Debugger] Error : ' + err + (err.stack ? ('\n' + err.stack) : ''));
+  else
+    console.error('[API Debugger] Error : ' + JSON.stringify(err));
+  return new RequestApiError('unknow', '' + JSON.stringify(err));
+}

+ 33 - 0
src/common/request/index.ts

@@ -0,0 +1,33 @@
+import type { DataModel } from "@imengyu/js-request-transform";
+import { RequestCoreInstance } from "./core/RequestCore";
+
+/**
+ * 基础请求模块
+ *
+ * 说明:
+ *  此处提供的是一个默认请求模块,演示了如何写自己的请求模块,
+ *  你可以参照这个类来写你自己的请求模块,并添加拦截器、错误处理、数据处理等等功能。
+ *
+ * Author: imengyu
+ * Date: 2022/03/25
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+
+/**
+ * 基础请求模块
+ * @deprecated 请使用 AuthServerRequestModule 或 AppServerRequestModule
+ */
+export class DefaultRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor() {
+    super();
+    this.config.requestInceptor = (url, req) => {
+      //登录相关Token添加
+      return { newUrl: url, newReq: req };
+    };
+  }
+}
+
+export default new DefaultRequestModule<DataModel>();

+ 52 - 0
src/common/request/utils/AllType.ts

@@ -0,0 +1,52 @@
+/**
+ * 请求工具所使用的类定义
+ *
+ * Author: imengyu
+ * Date: 2022/03/25
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+/**
+ * 空的结构
+ */
+export type TypeEmpty = Record<string, never>;
+
+/**
+ * 可保存数据
+ */
+export type TypeSaveable =
+  | TypeEmpty
+  | string
+  | number
+  | null
+  | undefined
+  | bigint
+  | boolean;
+/**
+ * 可保存数据
+ */
+export type TypeAll =
+  | TypeEmpty
+  | unknown
+  | object
+  | undefined
+  | string
+  | bigint
+  | number
+  | boolean;
+
+/**
+ * URL参数
+ */
+export interface QueryParams {
+  /**
+   * URL参数
+   */
+  [index: string]: TypeAll;
+}
+
+export interface HeaderType {
+  [key: string]: string;
+}

+ 77 - 0
src/common/request/utils/Utils.ts

@@ -0,0 +1,77 @@
+/**
+ * 请求工具所使用的工具函数
+ *
+ * 功能介绍:
+ *  提供了一些处理工具函数,方便使用。
+ *
+ * Author: imengyu
+ * Date: 2022/03/25
+ *
+ * Copyright (c) 2021 imengyu.top. Licensed under the MIT License.
+ * See License.txt in the project root for license information.
+ */
+
+/* eslint-disable no-bitwise */
+import type { KeyValue } from "@imengyu/js-request-transform/dist/DataUtils";
+import type { TypeSaveable } from "./AllType";
+
+export function isNullOrEmpty(val: unknown) {
+  return !val || typeof val === 'undefined' || val === '';
+}
+export function simpleClone<T>(obj: T) : T {
+  let temp: KeyValue|Array<KeyValue>|null = null;
+  if (obj instanceof Array) {
+    temp = obj.concat();
+  }
+  else if (typeof obj === 'object') {
+    temp = {} as KeyValue;
+    for (const item in obj) {
+      const val = (obj as unknown as KeyValue)[item];
+      if (val === null) temp[item] = null;
+      else (temp as KeyValue)[item] = simpleClone(val) as TypeSaveable;
+    }
+  } else {
+    temp = obj as unknown as KeyValue;
+  }
+  return temp as unknown as T;
+}
+/**
+ * 计算字符串的哈希值
+ * @param {string} str
+ */
+export function stringHashCode(str: string) {
+  return '' + (str.split("").reduce(function(a, b) {
+    a = (a << 5) - a + b.charCodeAt(0);
+    return (a & a);
+  }, 0));
+}
+export function appendGetUrlParams(url: string, key: string, value: any) {
+  if (!url.includes(`?${key}`) && !url.includes(`&${key}`)) {
+    if (url.includes('?'))
+      url = url + '&' + key + '=' + value;
+    else
+      url = url + '?' + key + '=' + value;
+  }
+  return url;
+}
+export function appendPostParams(source: any, key: string, value: any) {
+  if (typeof source === 'object' && source[key] === undefined)
+    source = { ...source, [key]: value };
+  return source;
+}
+
+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;
+}

+ 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,
+);

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

@@ -0,0 +1,37 @@
+$colors: (
+  primary: #d9492e,
+  primary-text: #D9492E,
+  second-text: #e6927e,
+  light-blue: #0059ff,
+  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: #FFFFFF,
+  base: #f6f2e7,
+  custom: #4A5061,
+  link: #0273F1,
+  light-light: #f5f1f1,
+  light: #F3F4F5,
+  dark: #F1F1F1,
+  dark-dark: #dfdfdf,
+  muted: #858585,
+  text: #333232,
+  second: #666464,
+  third: #999696,
+  forth: #CCC8C8,
+  place: #CCC8C8,
+  page: #FAFAFA,
+  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
+);

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

@@ -0,0 +1,69 @@
+//基础公共样式
+
+@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 *;
+
+.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 */
+	}
+}

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

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

@@ -0,0 +1,132 @@
+//弹性布局相关样式
+
+.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;
+	}
+}
+.align {
+	&-start {
+		align-items: flex-start;
+	}
+	&-center {
+		align-items: center;
+	}
+	&-end {
+		align-items: flex-end;
+	}
+  &-baseline {
+    align-items: baseline;
+  }
+}
+.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;
+  }
+}
+

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

@@ -0,0 +1,660 @@
+@use '../define/margin-padding.scss' as *;
+
+.h-100vh {
+  height: 100vh;
+}
+.h-0 {
+  height: 0;
+}
+.l-0 {
+  left: 0;
+}
+.r-0 {
+  right: 0;
+}
+.t-0 {
+  top: 0;
+}
+.b-0 {
+  bottom: 0;
+}
+.m-0 {
+  margin: 0 !important;
+}
+
+.m-1 {
+  margin: 0.25rem !important;
+}
+
+.m-2 {
+  margin: 0.5rem !important;
+}
+
+.m-3 {
+  margin: 1rem !important;
+}
+
+.m-4 {
+  margin: 1.5rem !important;
+}
+
+.m-5 {
+  margin: 3rem !important;
+}
+
+.m-auto {
+  margin: auto !important;
+}
+
+.mx-0 {
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+}
+
+.mx-1 {
+  margin-right: 0.25rem !important;
+  margin-left: 0.25rem !important;
+}
+
+.mx-2 {
+  margin-right: 0.5rem !important;
+  margin-left: 0.5rem !important;
+}
+
+.mx-3 {
+  margin-right: 1rem !important;
+  margin-left: 1rem !important;
+}
+
+.mx-4 {
+  margin-right: 1.5rem !important;
+  margin-left: 1.5rem !important;
+}
+
+.mx-5 {
+  margin-right: 3rem !important;
+  margin-left: 3rem !important;
+}
+
+.mx-auto {
+  margin-right: auto !important;
+  margin-left: auto !important;
+}
+
+.my-0 {
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+
+.my-1 {
+  margin-top: 0.25rem !important;
+  margin-bottom: 0.25rem !important;
+}
+
+.my-2 {
+  margin-top: 0.5rem !important;
+  margin-bottom: 0.5rem !important;
+}
+
+.my-3 {
+  margin-top: 1rem !important;
+  margin-bottom: 1rem !important;
+}
+
+.my-4 {
+  margin-top: 1.5rem !important;
+  margin-bottom: 1.5rem !important;
+}
+
+.my-5 {
+  margin-top: 3rem !important;
+  margin-bottom: 3rem !important;
+}
+
+.my-auto {
+  margin-top: auto !important;
+  margin-bottom: auto !important;
+}
+
+.mt-0 {
+  margin-top: 0 !important;
+}
+
+.mt-1 {
+  margin-top: 0.25rem !important;
+}
+
+.mt-2 {
+  margin-top: 0.5rem !important;
+}
+
+.mt-3 {
+  margin-top: 1rem !important;
+}
+
+.mt-4 {
+  margin-top: 1.5rem !important;
+}
+
+.mt-5 {
+  margin-top: 3rem !important;
+}
+
+.mt-auto {
+  margin-top: auto !important;
+}
+
+.mr-0 {
+  margin-right: 0 !important;
+}
+
+.mr-1 {
+  margin-right: 0.25rem !important;
+}
+
+.mr-2 {
+  margin-right: 0.5rem !important;
+}
+
+.mr-3 {
+  margin-right: 1rem !important;
+}
+
+.mr-4 {
+  margin-right: 1.5rem !important;
+}
+
+.mr-5 {
+  margin-right: 3rem !important;
+}
+
+.mr-auto {
+  margin-right: auto !important;
+}
+
+.mb-0 {
+  margin-bottom: 0 !important;
+}
+
+.mb-1 {
+  margin-bottom: 0.25rem !important;
+}
+
+.mb-2 {
+  margin-bottom: 0.5rem !important;
+}
+
+.mb-3 {
+  margin-bottom: 1rem !important;
+}
+
+.mb-4 {
+  margin-bottom: 1.5rem !important;
+}
+
+.mb-5 {
+  margin-bottom: 3rem !important;
+}
+
+.mb-auto {
+  margin-bottom: auto !important;
+}
+
+.ml-0 {
+  margin-left: 0 !important;
+}
+
+.ml-1 {
+  margin-left: 0.25rem !important;
+}
+
+.ml-2 {
+  margin-left: 0.5rem !important;
+}
+
+.ml-3 {
+  margin-left: 1rem !important;
+}
+
+.ml-4 {
+  margin-left: 1.5rem !important;
+}
+
+.ml-5 {
+  margin-left: 3rem !important;
+}
+
+.ml-auto {
+  margin-left: auto !important;
+}
+
+.m-n1 {
+  margin: -0.25rem !important;
+}
+
+.m-n2 {
+  margin: -0.5rem !important;
+}
+
+.m-n3 {
+  margin: -1rem !important;
+}
+
+.m-n4 {
+  margin: -1.5rem !important;
+}
+
+.m-n5 {
+  margin: -3rem !important;
+}
+
+.mx-n1 {
+  margin-right: -0.25rem !important;
+  margin-left: -0.25rem !important;
+}
+
+.mx-n2 {
+  margin-right: -0.5rem !important;
+  margin-left: -0.5rem !important;
+}
+
+.mx-n3 {
+  margin-right: -1rem !important;
+  margin-left: -1rem !important;
+}
+
+.mx-n4 {
+  margin-right: -1.5rem !important;
+  margin-left: -1.5rem !important;
+}
+
+.mx-n5 {
+  margin-right: -3rem !important;
+  margin-left: -3rem !important;
+}
+
+.my-n1 {
+  margin-top: -0.25rem !important;
+  margin-bottom: -0.25rem !important;
+}
+
+.my-n2 {
+  margin-top: -0.5rem !important;
+  margin-bottom: -0.5rem !important;
+}
+
+.my-n3 {
+  margin-top: -1rem !important;
+  margin-bottom: -1rem !important;
+}
+
+.my-n4 {
+  margin-top: -1.5rem !important;
+  margin-bottom: -1.5rem !important;
+}
+
+.my-n5 {
+  margin-top: -3rem !important;
+  margin-bottom: -3rem !important;
+}
+
+.mt-n1 {
+  margin-top: -0.25rem !important;
+}
+
+.mt-n2 {
+  margin-top: -0.5rem !important;
+}
+
+.mt-n3 {
+  margin-top: -1rem !important;
+}
+
+.mt-n4 {
+  margin-top: -1.5rem !important;
+}
+
+.mt-n5 {
+  margin-top: -3rem !important;
+}
+
+.mr-n1 {
+  margin-right: -0.25rem !important;
+}
+
+.mr-n2 {
+  margin-right: -0.5rem !important;
+}
+
+.mr-n3 {
+  margin-right: -1rem !important;
+}
+
+.mr-n4 {
+  margin-right: -1.5rem !important;
+}
+
+.mr-n5 {
+  margin-right: -3rem !important;
+}
+
+.mb-n1 {
+  margin-bottom: -0.25rem !important;
+}
+
+.mb-n2 {
+  margin-bottom: -0.5rem !important;
+}
+
+.mb-n3 {
+  margin-bottom: -1rem !important;
+}
+
+.mb-n4 {
+  margin-bottom: -1.5rem !important;
+}
+
+.mb-n5 {
+  margin-bottom: -3rem !important;
+}
+
+.ml-n1 {
+  margin-left: -0.25rem !important;
+}
+
+.ml-n2 {
+  margin-left: -0.5rem !important;
+}
+
+.ml-n3 {
+  margin-left: -1rem !important;
+}
+
+.ml-n4 {
+  margin-left: -1.5rem !important;
+}
+
+.ml-n5 {
+  margin-left: -3rem !important;
+}
+
+.p-0 {
+  padding: 0 !important;
+}
+
+.p-1 {
+  padding: 0.25rem !important;
+}
+
+.p-2 {
+  padding: 0.5rem !important;
+}
+
+.p-3 {
+  padding: 1rem !important;
+}
+
+.p-4 {
+  padding: 1.5rem !important;
+}
+
+.p-5 {
+  padding: 3rem !important;
+}
+
+.px-0 {
+  padding-right: 0 !important;
+  padding-left: 0 !important;
+}
+
+.px-1 {
+  padding-right: 0.25rem !important;
+  padding-left: 0.25rem !important;
+}
+
+.px-2 {
+  padding-right: 0.5rem !important;
+  padding-left: 0.5rem !important;
+}
+
+.px-3 {
+  padding-right: 1rem !important;
+  padding-left: 1rem !important;
+}
+
+.px-4 {
+  padding-right: 1.5rem !important;
+  padding-left: 1.5rem !important;
+}
+
+.px-5 {
+  padding-right: 3rem !important;
+  padding-left: 3rem !important;
+}
+
+.py-0 {
+  padding-top: 0 !important;
+  padding-bottom: 0 !important;
+}
+
+.py-1 {
+  padding-top: 0.25rem !important;
+  padding-bottom: 0.25rem !important;
+}
+
+.py-2 {
+  padding-top: 0.5rem !important;
+  padding-bottom: 0.5rem !important;
+}
+
+.py-3 {
+  padding-top: 1rem !important;
+  padding-bottom: 1rem !important;
+}
+
+.py-4 {
+  padding-top: 1.5rem !important;
+  padding-bottom: 1.5rem !important;
+}
+
+.py-5 {
+  padding-top: 3rem !important;
+  padding-bottom: 3rem !important;
+}
+
+.pt-0 {
+  padding-top: 0 !important;
+}
+
+.pt-1 {
+  padding-top: 0.25rem !important;
+}
+
+.pt-2 {
+  padding-top: 0.5rem !important;
+}
+
+.pt-3 {
+  padding-top: 1rem !important;
+}
+
+.pt-4 {
+  padding-top: 1.5rem !important;
+}
+
+.pt-5 {
+  padding-top: 3rem !important;
+}
+
+.pr-0 {
+  padding-right: 0 !important;
+}
+
+.pr-1 {
+  padding-right: 0.25rem !important;
+}
+
+.pr-2 {
+  padding-right: 0.5rem !important;
+}
+
+.pr-3 {
+  padding-right: 1rem !important;
+}
+
+.pr-4 {
+  padding-right: 1.5rem !important;
+}
+
+.pr-5 {
+  padding-right: 3rem !important;
+}
+
+.pb-0 {
+  padding-bottom: 0 !important;
+}
+
+.pb-1 {
+  padding-bottom: 0.25rem !important;
+}
+
+.pb-2 {
+  padding-bottom: 0.5rem !important;
+}
+
+.pb-3 {
+  padding-bottom: 1rem !important;
+}
+
+.pb-4 {
+  padding-bottom: 1.5rem !important;
+}
+
+.pb-5 {
+  padding-bottom: 3rem !important;
+}
+
+.pl-0 {
+  padding-left: 0 !important;
+}
+
+.pl-1 {
+  padding-left: 0.25rem !important;
+}
+
+.pl-2 {
+  padding-left: 0.5rem !important;
+}
+
+.pl-3 {
+  padding-left: 1rem !important;
+}
+
+.pl-4 {
+  padding-left: 1.5rem !important;
+}
+
+.pl-5 {
+  padding-left: 3rem !important;
+}
+
+
+
+.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;
+			}
+		}
+	}
+}

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

@@ -0,0 +1,15 @@
+.shadow-sm {
+  box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+}
+
+.shadow {
+  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+.shadow-lg {
+  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+}
+
+.shadow-none {
+  box-shadow: none !important;
+}

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

@@ -0,0 +1,77 @@
+//宽高, 大小相关样式
+@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 {
+	&-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.div($full-width, $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;
+		}
+	}
+}

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

@@ -0,0 +1,165 @@
+//文字相关样式
+
+.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: block;
+		white-space: nowrap;
+		/* #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;
+		/* #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;
+		/* #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>;

+ 41 - 0
src/common/utils/ArrayUtils.ts

@@ -0,0 +1,41 @@
+function remove<T>(array: T[], item: T) {
+  let index = array.indexOf(item);
+  if (index >= 0) {
+    array.splice(index, 1);
+    return true;
+  }
+  return false;
+}
+function removeAt<T>(array: T[], index: number) {
+  if (index >= 0) {
+    array.splice(index, 1);
+    return true;
+  }
+  return false;
+}
+function insert<T>(array: T[], i: number, item: T) {
+  if (i > array.length) {
+    array.push(item);
+    return array;
+  }
+  return array.splice(i, 0, item);
+}
+function contains<T>(array: T[], item: T) {
+  return array.indexOf(item) >= 0;
+}
+function clear<T>(array: T[]) {
+  return array.splice(0, array.length);
+}
+function addOnce<T>(array: T[], item: T) {
+  if (array.indexOf(item) >= 0) return array.length;
+  else return array.push(item);
+}
+
+export default {
+  addOnce,
+  clear,
+  contains,
+  insert,
+  removeAt,
+  remove,
+};

+ 104 - 0
src/common/utils/CheckUtils.ts

@@ -0,0 +1,104 @@
+/**
+ * Author: imengyu 2021-10-16
+ * 
+ * 检查工具类,此类提供了一些方法用于检查用户输入字符串是否满足要求。
+ */
+
+/**
+ * 检查用户输入字符串是否是合法身份证号
+ * @param {string} str 输入字符串
+ * @returns {boolean} 返回结果
+ */
+function checkIsCardNumber(str: string) {
+  return /^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(str);
+}
+
+/**
+ * 检查用户输入字符串是否是合法中文名字
+ * @param {string} str 
+ * @returns {boolean} 返回结果
+ */
+function checkIsChineseName(str: string) {
+  return /[\u4e00-\u9fa5]{2,5}/.test(str);
+}
+
+/**
+ * 检查用户输入字符串是否是中国手机号
+ * @param {string} str 
+ * @returns {boolean} 返回结果
+ */
+function checkIsChinesePhoneNumber(str: string) {
+  return /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(str); 
+}
+
+/**
+ * 检查用户输入字符串是否是网址
+ * @param {string} str
+ */
+function checkIsUrl(str: string) {
+  return /^(http|https):\/\/[a-zA-Z0-9]+\.[a-zA-Z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/.test(str); 
+}
+
+function checkIsImageFile(str: string) {
+  return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(str); 
+}
+/**
+ * 检查用户输入字符串是否为空
+ * @param {string} str 
+ * @returns {boolean} 返回结果
+ */
+function checkIsNotEmpty(str: string) {
+  return typeof str === 'string' && str != '';
+}
+
+/**
+ * 检查用户输入字符串是否为空(同样检查空格)
+ * @param {string} str 
+ * @returns {boolean} 返回结果
+ */
+function checkIsNotEmptyAndSpace(str: string) {
+  return typeof str === 'string' && str != '' && str.trim() != '';
+}
+
+/**
+ * 比较版本号
+ * @param v1 
+ * @param v2 
+ * @returns 
+ */
+function compareVersion(v1: any, v2: any) {
+  v1 = v1.split('.');
+  v2 = v2.split('.');
+  const len = Math.max(v1.length, v2.length)
+
+  while (v1.length < len) {
+    v1.push('0')
+  }
+  while (v2.length < len) {
+    v2.push('0')
+  }
+
+  for (let i = 0; i < len; i++) {
+    const num1 = parseInt(v1[i])
+    const num2 = parseInt(v2[i])
+
+    if (num1 > num2) {
+      return 1
+    } else if (num1 < num2) {
+      return -1
+    }
+  }
+
+  return 0
+}
+
+export default {
+  checkIsNotEmpty,
+  checkIsNotEmptyAndSpace,
+  checkIsCardNumber,
+  checkIsChineseName,
+	checkIsChinesePhoneNumber,
+  checkIsUrl,
+  checkIsImageFile,
+  compareVersion,
+}

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

@@ -0,0 +1,294 @@
+import StringUtils from "./StringUtils";
+
+/**
+ * 说明:通用工具类
+ */
+
+/* eslint-disable no-undefined */
+/**
+ * 空的结构
+ */
+export type TypeEmpty = Record<string, never>;
+
+/**
+  * 可保存数据
+  */
+export type TypeSaveable = TypeEmpty|string|number|null|undefined|bigint|boolean;
+/**
+  * 可保存数据
+  */
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type TypeAll = TypeEmpty|unknown|object|undefined|string|bigint|number|boolean;
+
+/**
+* 任意JSON数据
+*/
+export interface KeyValue {
+   [index: string]: TypeSaveable;
+}
+
+/**
+ * 数字补0,如果数字转为字符串后不足 `n` 位,则在它前面加 `0`
+ * @param num 数字
+ * @param n 指定字符串位数
+ * @returns 字符串
+ */
+function pad(num: number|string, n: number) : string {
+  let str = num.toString();
+  let len = str.length;
+  while (len < n) {
+    str = "0" + num;
+    len++;
+  }
+  return str;
+}
+
+/**
+ * 数字保留n位小数
+ * @param num 数字
+ * @param n 保留小数位数 
+ */
+function fixedNumber(num: number, n: number) : number {
+  return n > 0 ? parseFloat(num.toFixed(n)) : Math.round(num);
+}
+
+/**
+ * 检查是否定义
+ * @param obj 
+ */
+function isDefined(obj: TypeAll) : boolean {
+  return typeof obj !== 'undefined';
+}
+/**
+ * 字符串判空
+ * @param str 字符串
+ */
+function isNullOrEmpty(str: string|null|undefined|unknown) : boolean {
+  return StringUtils.isNullOrEmpty(str as string);
+}
+/**
+ * 判断是否定义并且不为 `null`
+ * @param v 要判断的数值
+ */
+function isDefinedAndNotNull(v: TypeAll) : boolean {
+  return v != null && typeof v != 'undefined';
+}
+/**
+ * 生成随机字符串
+ * @param len 随机字符串长度
+ * @returns 随机字符串
+ */
+function randomString(len?: number) : string {
+  len = len || 32;
+  const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
+  const maxPos = $chars.length;
+  let pwd = '';
+  for (let i = 0; i < len; i++) {
+    pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
+  }
+  return pwd;
+}
+
+/**
+ * 生成随机字符串
+ * @param len 随机字符串长度
+ * @returns 随机字符串
+ */
+function randomNumberString(len?: number) : string {
+  len = len || 32;
+  const $chars = '0123456789';
+  const maxPos = $chars.length;
+  let pwd = '';
+  for (let i = 0; i < len; i++) {
+    pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
+  }
+  return pwd;
+}
+
+/**
+ * 生成指定范围之内的随机数
+ * @param minNum 最小值
+ * @param maxNum 最大值
+ */
+function genRandom(minNum: number, maxNum: number) : number {
+  return Math.floor(Math.random()*(maxNum-minNum+1)+minNum); 
+}
+/**
+ * 生成不重复随机字符串
+ * @param randomLength 字符长度
+ */
+function genNonDuplicateID(randomLength: number) : string {
+  let idStr = Date.now().toString(36)
+  idStr += Math.random().toString(36).substr(3,randomLength)
+  return idStr
+}
+/**
+ * 生成不重复随机字符串
+ * @param randomLength 字符长度
+ */
+function genNonDuplicateIDHEX(randomLength: number) : string {
+  const idStr = genNonDuplicateID(randomLength);
+  return StringUtils.strToHexCharCode(idStr, false).substr(idStr.length - randomLength, randomLength);
+}
+
+function getDeviceUid() : string {
+  let uid = localStorage.getItem('DeviceUid');
+  if(!uid || uid === '') {
+    uid = genNonDuplicateID(16);
+    localStorage.setItem('DeviceUid', uid);
+  }
+  return uid;
+}
+
+/**
+ * 深克隆对象,数组
+ * @param obj 要克隆的对象
+ * @param deepArray 是否要深度克隆数组里的每个对象
+ */
+function clone(obj: TypeAll, deepArray = false): KeyValue|Array<KeyValue>|null {
+  let temp: KeyValue|Array<KeyValue>|null = null;
+  if (obj instanceof Array) {
+    if(deepArray) temp = (obj as KeyValue[]).map<KeyValue>((item) => clone(item, deepArray) as KeyValue)
+    else temp = obj.concat();
+  }
+  else if (typeof obj === 'object') {
+    temp = new Object() as KeyValue;
+    for (const item in obj) {
+      const val = (obj as KeyValue)[item];
+      if(val === null) temp[item] = null;
+      else (temp as KeyValue)[item] = clone(val, deepArray) as TypeSaveable;
+    }
+  }else 
+    temp = obj as KeyValue;
+  return temp;
+}
+
+/* eslint-disable */
+
+/**
+ * 深克隆对象
+ * @param obj 要克隆的对象(不能是数组,数组请使用 `clone` 方法)
+ * @param deepArray 是否要深度克隆数组属性里的每个对象
+ */
+function cloneObject<T>(obj: T, deepArray = false): T {
+  let temp = {};
+  for (const item in obj) {
+    const val = obj[item];
+    if(val === null) (temp as KeyValue)[item] = null;
+    else if (typeof val === 'object') (temp as KeyValue)[item] = clone(val, deepArray) as TypeSaveable;
+    else (temp as KeyValue)[item] = val as any;
+  }
+  return temp as T;
+}
+
+/**
+ * 合并两个对象(仅合并一级属性)
+ * @param obj1 
+ * @param obj2 
+ */
+function mergeObject(obj1 : Record<string, unknown>, obj2 : Record<string, unknown>) : Record<string, unknown> { 
+  for(const k in obj2)
+    obj1[k] = obj2[k];
+  return obj1;
+}
+/**
+ * 合并多个对象(仅合并一级属性)
+ * @param objs
+ */
+function mergeObjects(...objs : Record<string, unknown>[]) : Record<string, unknown> { 
+  if(objs.length < 1) 
+    return {};
+  if(objs.length < 2) 
+    return objs[0];
+  const o = objs[0];
+  for(let i = objs.length - 1; i > 0; i--)
+    mergeObject(o, objs[i]);
+  return o;
+}
+
+/* eslint-enable */
+
+export default {
+  pad,
+  isDefined,
+  isNullOrEmpty,
+  isDefinedAndNotNull,
+  /**
+   * 如果字符串为空,则返回undefined,否则返回字符串
+   * @param val 
+   */
+  emptyToUndefined(val: string) : undefined|string {
+    // eslint-disable-next-line no-undefined
+    return isNullOrEmpty(val) ? undefined : val;
+  },
+  /**
+   * 如果数字为null或小于等于0,则返回undefined,否则返回数字
+   * @param val 
+   */
+  zeroOrNullToUndefined(val?: number|null) : undefined|number {
+    // eslint-disable-next-line no-undefined
+    return (!val || val == 0) ? undefined : val;
+  },
+  clone,
+  /**
+   * 将源对象每个属性都复制到目标对象(不管目标对象有没有对应属性)
+   * @param setObj 目标对象
+   * @param sourceObj 源对象
+   */
+  cloneValue(setObj: { [index: string]: TypeAll}, sourceObj: { [index:  string]: TypeAll}) : void {
+    if(!setObj || !sourceObj) return;
+    Object.keys(setObj).forEach(function(key){
+      if(typeof sourceObj[key] != 'undefined') {
+        setObj[key] = sourceObj[key];
+      }
+    });
+  },
+  cloneObject,
+  /**
+   * 检查数组中是否全部是空字符串或null
+   * @param arr 要检查的数组
+   * @returns 
+   */
+  isArrayAllNullOrEmpty(arr : Array<unknown>) : boolean {
+    if(!arr)
+      return true;
+    for (let i = arr.length - 1; i >= 0; i--) {
+      if(arr[i] !== null && arr[i] !== "") {
+        return false;
+      }
+    }
+    return true;
+  },
+  /**
+   * 检查数组中是否有空值或字符串
+   * @param arr 要检查的数组
+   * @returns 
+   */
+  isArrayContainsNullOrEmpty(arr : Array<unknown>) : boolean {
+    if(!arr)
+      return false;
+    for (let i = arr.length - 1; i >= 0; i--) {
+      if(isNullOrEmpty(arr[i])) {
+        return true;
+      }
+    }
+    return false;
+  },
+  randomString,
+  randomNumberString,
+  getDeviceUid,
+  genRandom,
+  genNonDuplicateID,
+  genNonDuplicateIDHEX,
+  mergeObject,
+  mergeObjects,
+  fixedNumber,
+  /**
+   * 等待延时
+   */
+  waitTimeOut(timeOut: number) : Promise<void> {
+    return new Promise<void>((resolve) => {
+      setTimeout(() => resolve(), timeOut);
+    });
+  },
+}

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

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

+ 460 - 0
src/common/utils/DateUtils.ts

@@ -0,0 +1,460 @@
+import StringUtils from "./StringUtils";
+
+/**
+ * 说明:日期相关工具类
+ */
+
+/**
+ * 获取年份的中文表达形式
+ * @param {Number} year
+ */
+function getChineseYear(year: number) {
+  let arr1 = new Array('零', '一', '二', '三', '四', '五', '六', '七', '八', '九');
+  if (!year || isNaN(year)) {
+    return "零";
+  }
+  let english = year.toString().split("")
+  let result = "";
+  for (let i = 0; i < english.length; i++) {
+    let des_i = english.length - 1 - i;
+    result = result;
+    let arr1_index = english[des_i];
+    result = arr1[parseInt(arr1_index)] + result;
+  }
+  result = result.replace(/零(千|百|十)/g, '零').replace(/十零/g, '十');
+  result = result.replace(/零+/g, '零');
+  result = result.replace(/零亿/g, '亿').replace(/零万/g, '万');
+  result = result.replace(/亿万/g, '亿');
+  result = result.replace(/零+$/, '')
+  result = result.replace(/^一十/g, '十');
+  return result;
+}
+
+/**
+ * 获取某年的某月共多少天
+ * @param {number} year 
+ * @param {number} month
+ * @returns {number}
+ */
+function getMonthDays(year: number, month: number) {
+  switch (month + 1) {
+    case 1:
+    case 3:
+    case 5:
+    case 7:
+    case 8:
+    case 10:
+    case 12:
+      return 31;
+    case 4:
+    case 6:
+    case 9:
+    case 11:
+      return 30;
+    case 2:
+      return (year % 4 == 0 && year % 100 !== 0 || year % 400 == 0) ? 29 : 28;
+  }
+}
+
+/**
+ * 获取某一天(年月日)是星期几
+ * @param {number} year 
+ * @param {number} month 
+ * @param {number} date 
+ * @returns {number}
+ */
+function getDayWeekday(year: number, month: number, date: number) {
+  const dateNow = new Date(year, month - 1, date)
+  // 0-6, 0 is sunday
+  return dateNow.getDay()
+}
+
+/**
+ * 获取某一天所在周的日期
+ * @param {Date} date 
+ * @returns {{
+ * 	theDay: boolean,
+ * 	date: Date,
+ * 	today: boolean
+ * }[]}
+ */
+function getWeekDatesForDate(date: Date) {
+  const timeStamp = date.getTime()
+  const currentDay = date.getDay()
+  let dates = []
+  for (var i = 0; i < 7; i++) {
+    const _i = i - (currentDay + 6) % 7
+    const _isToday = _i === 0
+    const _date = new Date(timeStamp + 24 * 60 * 60 * 1000 * _i)
+    dates.push({
+      theDay: _isToday,  // 只是指当前查询的时间,在那一周的哪一天. 并不是指查询的这一天是否是今天
+      date: _date,
+      today: isToday(_date) // 是否是今天
+    })
+  }
+  return dates
+}
+
+/**
+ * 获取当前周的日期
+ * @returns 
+ */
+function getWeekDates() {
+  const new_Date = new Date()
+  return getWeekDatesForDate(new_Date)
+}
+
+/**
+ * 获取某一天所在周的日期
+ * @param {number} year 
+ * @param {number} month 
+ * @param {number} date 
+ * @returns 
+ */
+function getWeekDatesForYMD(year: number, month: number, date: number) {
+  const dateNow = new Date(year, month - 1, date)
+  return getWeekDatesForDate(dateNow)
+}
+
+/**
+ * 获取 开始日期 之后 第n周 的 日期
+ * @param {string} start 
+ * @param {number} diff 
+ * @returns 
+ */
+function getDatesAfterWeeks(start: string, diff: number) {
+  const _arr = start.replace(/-/g, '/').split('/')
+  const y = _arr[0]
+  const m = _arr[1]
+  const d = _arr[2]
+  const _start = new Date(parseInt(y), parseInt(m) - 1, parseInt(d))
+  const timeStamp = _start.getTime()
+  const date = new Date(timeStamp + diff * 7 * 24 * 60 * 60 * 1000)
+  return getWeekDatesForDate(date)
+}
+
+/**
+ * 获取当前是开始日期之后的第几周,如果大于total,则表示已经结束过时,如果小于0,则表示start还没有到来。
+ * 
+ * 我们这里需要注意的是,比如 开始日期是星期四(没有给出星期一的日期),下周的星期一应该是第2周,而不是还在第一周。(我们这里不仅仅只是差值周数计算,还需要受实际周的限制)
+ * 不能只是计算差值,如果给出的开始日期是星期一的,差值计算正确,如果不是,则需要考虑开始日期的星期
+ * @param {string} start 
+ * @param {number} total 
+ * @returns 
+ */
+function getCurrentWeekNumber(start: string, total: number) {
+  const _arr = start.replace(/-/g, '/').split('/')
+  const y = _arr[0]
+  const m = _arr[1]
+  const d = _arr[2]
+  const _start = new Date(parseInt(y), parseInt(m) - 1, parseInt(d))
+  let _timestamp = _start.getTime()
+  let day = _start.getDay() // 星期几
+  day = (day + 6) % 7  // 将星期几转化为距离星期一多少天
+  // 我们将开始时间修正到那一周的星期一
+  // 这里我们将星期天作为最后一天,星期一作为第一天
+  _timestamp = _timestamp - day * (24 * 60 * 60 * 1000)
+  // current
+  const dt = new Date()
+  const _y = dt.getFullYear()
+  const _m = dt.getMonth()
+  const _d = dt.getDate()
+  const today = new Date(_y, _m, _d)
+  const todayStamp = today.getTime()
+  const diff = todayStamp - _timestamp
+  if (diff < 0) {
+    // start还没有开始,未来返回-1
+    return -1
+  }
+  const weekStamp = 7 * 24 * 60 * 60 * 1000
+  let weekDiff = Math.floor(diff / weekStamp)
+  const more = diff % weekStamp
+  // if (more >= 24 * 60 * 60 * 1000) {
+  // weekDiff += 1
+  // }
+  // wo always need to plus 1 for weekDiff
+  const weekNumber = weekDiff + 1
+  if (weekNumber > total) {
+    // 已经过期
+    return -2
+  }
+  return weekNumber
+}
+
+/**
+ * 查询某日期是否是今天
+ * @param date 
+ * @returns 
+ */
+function isToday(date: Date) {
+  const dt = new Date();
+  const y = dt.getFullYear(); // 年
+  const _y = date.getFullYear();
+  const m = dt.getMonth(); // 月份从0开始的
+  const _m = date.getMonth();
+  const d = dt.getDate(); //日
+  const _d = date.getDate();
+  return (_y + '-' + _m + '-' + _d) === (y + '-' + m + '-' + d);
+}
+
+/**
+ * 获取 某年某月某日 是在 那一月 的第几周
+ * @param year 
+ * @param month 
+ * @param date 
+ * @returns {number}
+ */
+function getMonthWeek(year: number, month: number, date: number) {
+  /*  
+      month = 6 - w = 当前周的还有几天过完(不算今天)  
+      year + month 的和在除以7 就是当天是当前月份的第几周  
+  */
+  let dateNow = new Date(year, month - 1, date);
+  let w = dateNow.getDay(); //星期数
+  let d = dateNow.getDate();
+  return Math.ceil((d + 6 - w) / 7);
+}
+
+/**
+ * 获取 某年某月某日 是在 那一年 的第几周
+ * @param year 
+ * @param month 
+ * @param date 
+ * @returns 
+ */
+function getYearWeek(year: number, month: number, date: number) {
+  /*  
+      dateNow是当前日期 
+      dateFirst是当年第一天  
+      dataNumber是当前日期是今年第多少天  
+      用dataNumber + 当前年的第一天的周差距的和在除以7就是本年第几周
+  */
+  let dateNow = new Date(year, month - 1, date);
+  let dateFirst = new Date(year, 0, 1);
+  let dataNumber = Math.round((dateNow.valueOf() - dateFirst.valueOf()) / 86400000);
+  return Math.ceil((dataNumber + ((dateFirst.getDay() + 1) - 1)) / 7);
+}
+
+/**
+ * 获取今天是星期几
+ * @returns 
+ */
+function getCurrentWeekday() {
+  const myDate = new Date()
+  let days : number|string = myDate.getDay()
+  const number = days
+  switch (days) {
+    case 1:
+      days = '星期一'
+      break
+    case 2:
+      days = '星期二'
+      break
+    case 3:
+      days = '星期三'
+      break
+    case 4:
+      days = '星期四'
+      break
+    case 5:
+      days = '星期五'
+      break
+    case 6:
+      days = '星期六'
+      break
+    case 0:
+      days = '星期日'
+      break
+  }
+  return {
+    number: number,
+    weekday: days
+  }
+}
+
+/**
+ * 获取今天的 年月日
+ * @returns 
+ */
+function getCurrentYearMonthDay() {
+  const date = new Date()
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  return {
+    year: year,
+    month: month,
+    day: day
+  }
+}
+
+/**
+ * 检查是不是在指定时间范围内
+ * 
+ * 只能比较同一天之内的时间,不跨天比较
+ * 24小时制,最大时间23:59
+ * @param start 
+ * @param end 
+ * @returns 
+ */
+function inTimePeriod(start: string, end: string) {
+  const now = new Date()
+  const nowH = now.getHours() * 1
+  const nowM = now.getMinutes() * 1
+  const _now = nowH * 60 + nowM
+  const startArr = start.split(":")
+  const startH = parseInt(startArr[0]) * 1
+  const startM = parseInt(startArr[1]) * 1
+  const _start = startH * 60 + startM
+  const endArr = end.split(":")
+  const endH = parseInt(endArr[0]) * 1
+  const endM = parseInt(endArr[1]) * 1
+  const _end = endH * 60 + endM
+  if (_now > _end) {
+    // 已经过时
+    return -1
+  }
+  if (_now >= _start && _now <= _end) {
+    // 正在进行
+    return 0
+  }
+  // 尚未开始
+  return 1
+}
+
+/**
+ * 获取星期几的中文表达字符串
+ * @param week (0-6)
+ */
+function getWeekdayStr(week: number) {
+
+  switch (week) {
+    case 1:
+      return '星期一'
+    case 2:
+      return '星期二'
+    case 3:
+      return '星期三'
+    case 4:
+      return '星期四'
+    case 5:
+      return '星期五'
+    case 6:
+      return '星期六'
+    case 0:
+      return '星期日'
+  }
+  return '?' + week
+}
+
+/**
+ * 计算两个日期之间的天数差距
+ * @param s1
+ * @param s2
+ */
+function getDayDiff(s1: Date, s2: Date) {
+  var days = s2.getTime() - s1.getTime();
+  var time = Math.floor(days / (1000 * 60 * 60 * 24));
+  return time;
+}
+
+/**
+ * 计算天数与今天的差距,将返回: x天前、前天 、昨天、今天、明天、后天、x天后
+ * @param y 年
+ * @param m 月,与 Date.getMonth 一样需要减一
+ * @param d 日
+ */
+function getDayGap(y: number, m: number, d: number) {
+  let now = new Date();
+  let diffDay = getDayDiff(new Date(), new Date(y, m, d)) + 1;
+  if (diffDay == 0) return "今天"
+  else if (diffDay == 1) return "明天"
+  else if (diffDay == 2) return "后天"
+  else if (diffDay == -1) return "昨天"
+  else if (diffDay == -2) return "前天"
+  else if (diffDay > 0) {
+    if (diffDay > 365) return `${Math.floor(diffDay / 365)}年${diffDay % 365}天后`;
+    else return `${diffDay}天后`;
+  }
+  else if (diffDay < 0) {
+    if (diffDay < -365) return `${Math.floor(-diffDay / 365)}年${-diffDay % 365}天前`;
+    else return `${-diffDay}天前`;
+  }
+}
+
+const DateUtils = {
+  FormatStrings: {
+    YearChanese: "YYYY年MM月dd日",
+  },
+  /**
+   * 日期加上指定天数
+   * @param date 日期
+   * @param days 添加的天数
+   */
+  dateAddDays(date: Date, days = 1) {
+    return new Date(date.getTime() + days * 86400000);
+  },
+  /**
+   * 日期加上指定小时
+   * @param date 日期
+   * @param hours 添加的小时数
+   */
+  dateAddHours(date: Date, hours = 1) {
+    return new Date(date.getTime() + hours * 1000 * 60 * 60);
+  },
+  /**
+   * 格式化日期
+   * @param date 日期
+   * @param formatStr 格式
+   * @returns 
+   */
+  formatDate(date: Date, formatStr = "YYYY-MM-dd HH:mm:ss"): string {
+    if (!date) 
+      return "";
+    if (!(date instanceof Date) )
+      return "!Date";
+    let str = formatStr ? formatStr : "YYYY-MM-dd HH:mm:ss";
+    //let Week = ['日','一','二','三','四','五','六'];
+    str = str.replace(/yyyy|YYYY/, date.getFullYear().toString());
+    str = str.replace(/MM/, StringUtils.pad(date.getMonth() + 1, 2));
+    str = str.replace(/M/, (date.getMonth() + 1).toString());
+    str = str.replace(/dd|DD/, StringUtils.pad(date.getDate(), 2));
+    str = str.replace(/d/, date.getDate().toString());
+    str = str.replace(/HH/, StringUtils.pad(date.getHours(), 2));
+    str = str.replace(/hh/, StringUtils.pad(date.getHours() > 12 ? date.getHours() - 12 : date.getHours(), 2));
+    str = str.replace(/mm/, StringUtils.pad(date.getMinutes(), 2));
+    str = str.replace(/ii/, StringUtils.pad(date.getMinutes(), 2));
+    str = str.replace(/ss/, StringUtils.pad(date.getSeconds(), 2));
+    return str;
+  },
+  /**
+   * 转换字符串日期为 Date
+   * @param dateString 日期字符串
+   */
+  parseDate(dateString: string | Date | number, format?: string) {
+    if (typeof dateString === 'object' && dateString instanceof Date)
+      return dateString;
+    if (typeof dateString === 'number')
+      return new Date(dateString);
+    return new Date(dateString.replace(/-/g, '/'));
+  },
+  getChineseYear,
+  getMonthDays,
+  getDayWeekday,
+  getWeekDatesForDate,
+  getWeekDates,
+  getWeekDatesForYMD,
+  getDatesAfterWeeks,
+  getCurrentWeekNumber,
+  isToday,
+  getMonthWeek,
+  getYearWeek,
+  getCurrentWeekday,
+  getCurrentYearMonthDay,
+  inTimePeriod,
+  getWeekdayStr,
+  getDayDiff,
+  getDayGap,
+}
+
+export default DateUtils;

+ 78 - 0
src/common/utils/DialogAction.ts

@@ -0,0 +1,78 @@
+import { type App } from "vue";
+
+/**
+ * 说明:对话框相关的封装
+ */
+
+/**
+ * 显示一个文本提示框
+ * @param content - 要显示的提示内容
+ */
+function toast(content: string) {
+  if (typeof content!=='string')
+    content = '' + content;
+  uni.showToast({
+    title: content,
+    icon: 'none',
+  })
+}
+/**
+ * 显示一个确认对话框
+ * @param option - 对话框的配置选项
+ * @param option.title - 对话框的标题,可选
+ * @param option.content - 对话框的内容,可选
+ */
+function alert(option: {
+  title?: string,
+  content?: string,
+}) {
+  uni.showModal({
+    title: option.title,
+    content: option.content,
+  })
+};
+/**
+ * 显示一个确认对话框
+ * @param option - 对话框的配置选项
+ * @param option.title - 对话框的标题,可选
+ * @param option.content - 对话框的内容,可选
+ * @param option.cancelText - 取消按钮的文本,可选
+ * @param option.confirmText - 确认按钮的文本,可选
+ * @returns - 返回一个 Promise 对象,当用户点击确定按钮时,Promise 的结果为 true,否则为 false
+ */
+function confirm(option: {
+  title?: string,
+  content?: string,
+  cancelText?: string, 
+  confirmText?: string,
+}) {
+  return new Promise<boolean>((resolve, reject) => {
+    uni.showModal({
+      title: option.title || '',
+      content: option.content || '',
+      showCancel: true,
+      cancelText: option.cancelText || '取消',
+      confirmText: option.confirmText || '确定',
+      success(res) {
+        resolve(res.confirm);
+      },
+      fail(res) {
+        reject(res);
+      }
+    })
+  })
+}
+
+export {
+  toast,
+  alert,
+  confirm, 
+}
+
+export default {
+  install(app: App<Element>) : void {
+    app.config.globalProperties.$toast = toast;
+    app.config.globalProperties.$alert = alert;
+    app.config.globalProperties.$confirm = confirm;
+  },
+}

+ 98 - 0
src/common/utils/PageAction.ts

@@ -0,0 +1,98 @@
+import { type App } from "vue";
+
+/**
+ * 说明:页面导航相关函数封装。
+ */
+
+/**
+ * 页面跳转: 后退至上一个页面。
+ */
+function back() {
+  uni.navigateBack({ delta: 1 });
+}
+/**
+ * 页面跳转: 后退并返回数据至上一个页面的 onPageBack 方法。
+ * @param data 要返回的数据
+ */
+function backReturnData(data: Record<string, unknown>) {
+  var pages = getCurrentPages(); // 获取页面栈
+  var prevPage = pages[pages.length - 2] as { $vm: Record<string, unknown> }; // 上一个页面
+
+  for (const key in data) {
+    if (Object.prototype.hasOwnProperty.call(data, key)) {
+      prevPage.$vm[key] = data[key];
+    }
+  }
+
+  uni.navigateBack({ delta: 1 });
+}
+/**
+ * 页面跳转: 跳转到指定页面
+ * @param url 页面路径
+ * @param data 要传递的数据
+ */
+function navTo(url: string, data: Record<string, unknown> = {}) {
+  var dataString = '';
+
+  for (const key in data) {
+    if (Object.prototype.hasOwnProperty.call(data, key))
+      dataString += `&${key}=${data[key]}`;
+  }
+
+  uni.navigateTo({ 
+    url: url + '?' + dataString,
+    fail: (err) => {
+      console.error('页面跳转失败:', err);
+      uni.showToast({
+        title: '跳转失败',
+        icon: 'none',
+      });
+    },
+  });
+}
+/**
+ * 页面数据传递: 调用上一个页面的 onPageBack 方法
+ * @param name 方法名
+ * @param data 要传递的数据
+ */
+function callPrevOnPageBack(name: string, data: Record<string, unknown>) {
+  var pages = getCurrentPages(); // 获取页面栈
+  var prevPage = pages[pages.length - 2] as { $vm: Record<string, unknown> }; // 上一个页面
+
+  if (typeof prevPage.$vm.onPageBack === 'function')
+    prevPage.$vm.onPageBack(name, data);
+}
+/**
+ * 页面跳转: 调用上一个页面的 onPageBack 方法并返回至上一个页面
+ * @param name 方法名
+ * @param data 要传递的数据
+ */
+function backAndCallOnPageBack(name: string, data: Record<string, unknown>) {
+  var pages = getCurrentPages(); // 获取页面栈
+  var prevPage = pages[pages.length - 2] as { $vm: Record<string, unknown> }; // 上一个页面
+
+  if (typeof prevPage.$vm.onPageBack === 'function')
+    prevPage.$vm.onPageBack(name, data);
+
+  uni.navigateBack({ delta: 1 });
+}
+
+export {
+  back,
+  backReturnData,
+  backAndCallOnPageBack, 
+  navTo,
+  callPrevOnPageBack,
+}
+
+export default {
+  install(app: App<Element>) : void {
+    app.config.globalProperties.$p = {
+      back,
+      backReturnData,
+      backAndCallOnPageBack,
+      callPrevOnPageBack,
+      navTo,
+    };
+  },
+}

+ 214 - 0
src/common/utils/StringUtils.ts

@@ -0,0 +1,214 @@
+/**
+ * 说明:字符串工具类
+ */
+
+/**
+ * 得到字符串含有某个字符的个数  
+ * @param str 字符串
+ * @param char 某个字符
+ * @returns 个数  
+ */
+function getCharCount(str: string, char: string) : number {
+  const regex = new RegExp(char, 'g'); // 使用g表示整个字符串都要匹配
+  const result = str.match(regex);          //match方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
+  const count=!result ? 0 : result.length;
+  return count;
+}
+/**
+* 判断字符串是否是 Base64 编码
+* @param {String} str 
+*/
+function isBase64(str: string) : boolean {
+  return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/.test(str);
+}
+/**
+ * 检测字符串是否是一串数字
+ * @param {String} val 
+ */
+function isNumber(val: string) : boolean {
+  const regPos = /^\d+(\.\d+)?$/; //非负浮点数
+  const regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
+  if (regPos.test(val) || regNeg.test(val)) {
+    return true;
+  } else {
+    return false;
+  }
+}
+/**
+ * 检查字符串是否是中国的11位手机号
+ * @param str 字符串
+ */
+function isChinaPoneNumber(str: string) : boolean {
+  if (!/^[1][3,4,5,7,8][0-9]{9}$/.test(str)) {
+      return false;
+  } else {
+      return true;
+  }
+}
+/**
+ * 检查字符串是否是邮箱
+ * @param str 字符串
+ */
+function isEmail(str: string) : boolean {
+  const re = /^\w+((-\w+)|(\.\w+))*@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/;
+  if (re.test(str) !== true) {
+    return false;
+  }else{
+    return true;
+  }
+}
+/**
+ * 将字符串转为16进制字符串
+ * @param str 字符串
+ */
+function strToHexCharCode(str: string, with0x = true): string {
+  if(str === "")
+    return "";
+  const hexCharCode = [];
+  if(with0x) hexCharCode.push("0x"); 
+  for(let i = 0; i < str.length; i++) {
+    hexCharCode.push((str.charCodeAt(i)).toString(16));
+  }
+  return hexCharCode.join("");
+}
+/**
+ * 数字补0
+ * @param num 数字
+ * @param n 如果数字不足n位,则自动补0
+ */
+function pad(num: number, n: number) : string {
+  let strNum = num.toString();
+  let len = strNum.length;
+  while (len < n) {
+    strNum = "0" + strNum;
+    len++;
+  }
+  return strNum;
+}
+/**
+ * 按千位逗号分割
+ * @param s 需要格式化的数值.
+ * @param type 判断格式化后是否需要小数位.
+ */
+function formatNumberWithComma(s: string, addComma: boolean) : string {
+  if (/[^0-9]/.test(s))
+      return "0";
+  if (s === null || s === "")
+      return "0";
+  s = s.toString().replace(/^(\d*)$/, "$1.");
+  s = (s + "00").replace(/(\d*\.\d\d)\d*/, "$1");
+  s = s.replace(".", ",");
+
+  const re = /(\d)(\d{3},)/;
+  while (re.test(s))
+      s = s.replace(re, "$1,$2");
+  s = s.replace(/,(\d\d)$/, ".$1");
+  if (!addComma) { // 不带小数位(默认是有小数位)
+      const a = s.split(".");
+      if (a[1] === "00") {
+          s = a[0];
+      }
+  }
+  return s;
+}
+
+/**
+ * 格式化显示大数字
+ * @param Number 数字
+ */
+function formatHugeNumber(Number: number) : string {
+  if (Number >= 1000000000000)
+    return Number.toExponential(2).replace(/e\+/g, 'x10^');
+  if (Number >= 1000000000)
+    return (Number / 1000000000).toFixed(2) + 'B';
+  if (Number >= 1000000)
+    return (Number / 1000000).toFixed(2) + 'M';
+  return Number.toFixed(2);
+}
+
+const StringUtils = {
+  formatDate(date: Date, formatStr = "YYYY-MM-dd HH:mm:ss") : string {
+    let str = formatStr ? formatStr : "YYYY-MM-dd HH:mm:ss";
+    //let Week = ['日','一','二','三','四','五','六'];
+    str = str.replace(/yyyy|YYYY/, date.getFullYear().toString());
+    str = str.replace(/MM/, pad(date.getMonth() + 1, 2));
+    str = str.replace(/M/, (date.getMonth() + 1).toString());
+    str = str.replace(/dd|DD/, pad(date.getDate(), 2));
+    str = str.replace(/d/, date.getDate().toString());
+    str = str.replace(/HH/, pad(date.getHours(), 2));
+    str = str.replace(
+      /hh/,
+      pad(date.getHours() > 12 ? date.getHours() - 12 : date.getHours(), 2)
+    );
+    str = str.replace(/mm/, pad(date.getMinutes(), 2));
+    str = str.replace(/ii/, pad(date.getMinutes(), 2));
+    str = str.replace(/ss/, pad(date.getSeconds(), 2));
+    return str;
+  },
+  /**
+   * 字符串判空
+   * @param str 字符串
+   */
+  isNullOrEmpty(str: string | undefined | null | false) : boolean {
+    return !str || typeof str === 'undefined' || str === ''
+  },
+  isBase64,
+  isNumber,
+  isChinaPoneNumber,
+  isEmail,
+  strToHexCharCode,
+  pad,
+  formatHugeNumber,
+  formatNumberWithComma,
+  getFileName(path: string) : string {
+    let pos = path.lastIndexOf('/');
+    if(pos < 0) pos = path.lastIndexOf('\\');
+    return path.substring(pos + 1);  
+  },
+  getFileExt(path: string) : string {
+    return path.substring(path.lastIndexOf('.') + 1);
+  },
+  getCharCount,
+  getFileSizeStringAuto(filesize: number) : string {
+    let sizeStr = '';
+    if(filesize >= 1073741824){
+      filesize = Math.round(filesize/1073741824*100)/100;
+      sizeStr = filesize + "GB";
+    }else if(filesize >= 1048576) {
+      filesize = Math.round(filesize/1048576*100)/100;
+      sizeStr = filesize + "MB";
+    }else{
+      filesize = Math.round(filesize/1024*100)/100;
+      sizeStr = filesize + "KB";
+    }
+    return sizeStr;
+  },
+  /**
+   * 移除URL的地址部分,只保留路径
+   * @param str 原URL
+   * @returns 
+   */
+  removeUrlOrigin(str: string) : string {
+    if(str.startsWith('http://') || str.startsWith('https://') || str.startsWith('fts://') || str.startsWith('ftps://')) {
+      str = str.substr(str.indexOf('://') + 3);
+      str = str.substr(str.indexOf('/') + 1);
+    } 
+    return str;
+  },
+  /**
+   * 将手机号转换成 xxx******xx
+   * @param str 手机号
+   */
+  convertPhoneToSecret6(str: string): string{
+    return str.replace(/^(\d{3})(\d*)(\d{2}$)/, "$1******$3");
+  },
+  /**
+   * 将手机号转换成 尾号xxxx用户
+   * @param str 手机号
+   */
+  convertPhoneToUserName(str: string): string{
+    return '尾号' + str.substring(str.length - 4) + '用户';
+  },
+}
+
+export default StringUtils;

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

+ 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 './common/utils/DialogAction';
+import PageAction from './common/utils/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": "xiangan",
+	"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": "wx6da2b44a4ddf5248",
+		"setting": {
+			"urlCheck": false,
+			"es6": true,
+			"postcss": true,
+			"minified": true,
+      "ignoreDevUnusedFiles": false
+		},
+		"permission": {
+			"scope.userLocation": {
+				"desc": "你的位置信息将用于小程序位置接口的效果展示"
+			}
+		},
+		"requiredPrivateInfos": [
+      "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"
+				}
+			}
+		}
+	}
+}

+ 68 - 0
src/package.json

@@ -0,0 +1,68 @@
+{
+    "id": "y-video-slide",
+    "displayName": "模仿抖音视频滑动组件",
+    "version": "1.1.1",
+    "description": "模仿抖音视频滑动组件",
+    "keywords": [
+        "视频",
+        "滑动",
+        "抖音",
+        "点赞",
+        "评论"
+    ],
+    "repository": "",
+    "engines": {
+        "HBuilderX": "^3.1.0"
+    },
+    "dcloudext": {
+        "category": [
+            "前端组件",
+            "通用组件"
+        ]
+    },
+    "uni_modules": {
+        "dependencies": [],
+        "encrypt": [],
+        "platforms": {
+            "cloud": {
+                "tcb": "y",
+                "aliyun": "y"
+            },
+            "client": {
+                "Vue": {
+                    "vue2": "y",
+                    "vue3": "y"
+                },
+                "App": {
+                    "app-vue": "y",
+                    "app-nvue": "y"
+                },
+                "H5-mobile": {
+                    "Safari": "y",
+                    "Android Browser": "y",
+                    "微信浏览器(Android)": "y",
+                    "QQ浏览器(Android)": "y"
+                },
+                "H5-pc": {
+                    "Chrome": "y",
+                    "IE": "y",
+                    "Edge": "y",
+                    "Firefox": "y",
+                    "Safari": "y"
+                },
+                "小程序": {
+                    "微信": "y",
+                    "阿里": "y",
+                    "百度": "y",
+                    "字节跳动": "y",
+                    "QQ": "y"
+                },
+                "快应用": {
+                    "华为": "y",
+                    "联盟": "y"
+                }
+            }
+        }
+    },
+    "name": "模仿抖音短视频滑动组件(支持H5、微信小程序)"
+}

+ 120 - 0
src/pages.json

@@ -0,0 +1,120 @@
+{
+  "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/home",
+      "style": {
+        "navigationBarTitleText": "闽南文化生态保护-首页",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/travel",
+      "style": {
+        "navigationBarTitleText": "闽南文化生态保护-文旅",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/discover",
+      "style": {
+        "navigationBarTitleText": "闽南文化生态保护-发现",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/inhert",
+      "style": {
+        "navigationBarTitleText": "闽南文化生态保护-传承",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/article/details",
+      "style": {
+        "navigationBarTitleText": "新闻详情"
+      }
+    },
+    {
+      "path": "pages/article/list",
+      "style": {
+        "navigationBarTitleText": "文章列表页"
+      }
+    },
+    {
+      "path": "pages/article/editor/editor",
+      "style": {
+        "navigationBarTitleText": "编辑文章",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/article/editor/preview",
+      "style": {
+        "navigationBarTitleText": "预览文章",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/video/details",
+      "style": {
+        "navigationBarTitleText": "视频播放页"
+      }
+    },
+    {
+      "path": "pages/video/list",
+      "style": {
+        "navigationBarTitleText": "视频列表页"
+      }
+    },
+    {
+      "path": "pages/user/index",
+      "style": {
+        "navigationBarTitleText": "个人中心",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/user/login",
+      "style": {
+        "navigationBarTitleText": "登录",
+        "enablePullDownRefresh": false
+      }
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTextStyle": "white",
+    "navigationBarTitleText": "uni-app",
+    "navigationBarBackgroundColor": "#d9492e",
+    "backgroundColor": "#F8F8F8"
+  },
+  "uniIdRouter": {},
+  "tabBar": {
+    "custom": true,
+    "list": [
+      {
+        "pagePath": "pages/home",
+        "text": "首页"
+      },
+      {
+        "pagePath": "pages/discover",
+        "text": "发现"
+      },
+      {
+        "pagePath": "pages/inhert",
+        "text": "传承"
+      },
+      {
+        "pagePath": "pages/travel",
+        "text": "翔旅"
+      },
+      {
+        "pagePath": "pages/user/index",
+        "text": "我的"
+      }
+    ]
+  }
+}

+ 92 - 0
src/pages/article/details.vue

@@ -0,0 +1,92 @@
+<template>
+  <view class="main">
+    <SimplePageContentLoader :loader="loader">
+      <template v-if="loader.content.value">
+        <view class="article">
+          <view class="title">{{ loader.content.value.title }}</view>
+          <view class="info">
+            <view class="author">{{ loader.content.value.author }}</view>
+            <view class="time">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd HH:ii:ss') }}</view>
+          </view>
+          <view class="content">
+            <u-parse :content="loader.content.value.content" :tagStyle="commonParserStyle"></u-parse>
+          </view>
+        </view>
+        <view class="bottom-actions">
+          <view class="action">
+            <text class="iconfont icon-read"></text>
+            {{ loader.content.value.views }}
+          </view>
+          <view class="action">
+            <text class="iconfont icon-like" v-if="!loader.content.value.isLike"></text>
+            <text class="iconfont icon-liked" v-else></text>
+            {{ loader.content.value.likes }}
+          </view>
+        </view>
+      </template>
+    </SimplePageContentLoader>
+  </view>
+</template>
+
+<script setup lang="ts">
+import type { GetContentDetailItem } from "@/api/CommonContent";
+import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
+import { onLoad } from "@dcloudio/uni-app";
+import NewsIndexContent from "@/api/news/NewsIndexContent";
+import commonParserStyle from "@/common/style/commonParserStyle";
+import { DataDateUtils } from "@imengyu/js-request-transform";
+
+const loader = useSimplePageContentLoader<
+  GetContentDetailItem, 
+  { id: number }
+>(async (params) => {
+  if (!params)
+    throw new Error("!params");
+  const res = await NewsIndexContent.getContentDetail(params.id);
+  //console.log(res);
+  return res;
+});
+
+onLoad((options) => {
+  loader.loadData({
+    id: Number(options?.id),
+  })
+});
+</script>
+
+<style lang="scss" scoped>
+.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;
+    }
+  }
+}
+</style>

+ 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 '@/common/utils/DialogAction';
+import { back, backAndCallOnPageBack } from '@/common/utils/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>

+ 106 - 0
src/pages/article/food/index.vue

@@ -0,0 +1,106 @@
+<template>
+  <view class="main">
+    <view class="search">
+      <uni-search-bar 
+        v-model="searchValue"
+        radius="100" 
+        bgColor="#fff" 
+        placeholder="搜一搜" 
+        clearButton="auto" 
+        cancelButton="none"
+        @confirm="search"
+      />
+    </view>
+    <u-tabs 
+      :current="currentCategoryId"
+      :list="categoryList" 
+      lineWidth="30"
+      lineColor="rgb(255, 135, 25)"
+      :activeStyle="{
+        color: '#000',
+        fontWeight: 'bold',
+        transform: 'scale(1.05)'
+      }"
+      :inactiveStyle="{
+        color: '#606266',
+        transform: 'scale(1)'
+      }"
+      :scrollable="false"
+      class="top-tab"
+      @click="tabClick"
+    />
+    <view class="post-list">
+      <view
+        v-for="item in listLoader.list.value" 
+        :key="item.id"
+        class="item" 
+        @click="goDetails(item.id)"
+      >
+        <view class="image-wrap" :style="{backgroundImage:'url('+item.image+')'}">
+          <view class="like" :class="{liked: item.isLike}">
+            <text class="iconfont icon-liked" v-if="item.isLike"></text>
+            <text class="iconfont icon-like" v-else></text>
+            <text>{{ item.likes }}</text>
+          </view>
+        </view>
+        <view class="desc ellipsis-2">{{ item.title }}</view>
+      </view>
+    </view>
+    <SimplePageListLoader :loader="listLoader" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { navTo } from '@/common/utils/PageAction';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import { GetContentListItem, GetContentListParams } from '@/api/CommonContent';
+import NewsIndexContent from '@/api/news/NewsIndexContent';
+
+const searchValue = ref('');
+const currentCategoryId = ref(3);
+const categoryList = [
+  {
+    id: 1,
+    name: '美好时光'
+  },
+  {
+    id: 2,
+    name: '伴手好礼'
+  }, {
+    id: 3,
+    name: '特色佳肴'
+  }, {
+    id: 4,
+    name: '海鲜盛宴'
+  }
+];
+
+const listLoader = useSimplePageListLoader<GetContentListItem>(8, async (page, pageSize) => {
+  const res = await NewsIndexContent.getContentList(new GetContentListParams().setSelfValues({
+    keywords: searchValue.value,
+    mainBodyColumnId: 303
+  }), page, pageSize);
+  return res.list;
+});
+
+watch(currentCategoryId, () => {
+  listLoader.loadData(undefined, true);
+}, { immediate: true });
+
+function goDetails(id: number) {
+  navTo('../details', { id })
+}
+function search() {
+  listLoader.loadData(undefined, true);
+}
+function tabClick(e: { index: number }) {
+  currentCategoryId.value = e.index;
+}
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 95 - 0
src/pages/article/list.vue

@@ -0,0 +1,95 @@
+<template>
+  <view class="article_list">
+    <view class="search">
+      <uni-search-bar 
+        v-model="searchText"
+        radius="100"
+        bgColor="#fff"
+        placeholder="搜一搜" 
+        clearButton="auto"
+        cancelButton="none"
+        @confirm="search"
+      />
+    </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" />
+  </view>
+</template>
+
+<script setup lang="ts">
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+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 '@/common/utils/PageAction';
+import { onLoad } from '@dcloudio/uni-app';
+
+const searchText = ref('');
+const listLoader = useSimplePageListLoader<{
+  id: number,
+  image: string,
+  title: string,
+  date: string
+}, {
+  modelId: number|undefined, 
+  mainBodyColumnId: number|undefined,
+}>(8, async (page, pageSize, params) => {
+  if (!params || !params.modelId || !params.mainBodyColumnId)
+    throw new Error("未传入参数,当前页面需要参数");
+  const res = await CommonContent.getContentList(new GetContentListParams().setSelfValues({
+    keywords: searchText.value,
+    type: GetContentListParams.TYPE_ARTICLE,
+    modelId: params.modelId, 
+    mainBodyColumnId: params.mainBodyColumnId ,
+  }), page, pageSize);
+  return res.list.map((item) => {
+    return {
+      id: item.id,
+      image: item.image,
+      title: item.title,
+      date: DataDateUtils.formatDate(item.publishAt, 'YYYY-MM-dd'),
+    }
+  })
+});
+
+function goDetail(id: number) {
+  navTo('/pages/article/details', { id });
+}
+function search() {
+  listLoader.loadData(undefined, true);
+}
+
+onLoad((query) => {
+  if (query?.title) {
+    uni.setNavigationBarTitle({
+      title: query.title,
+    });
+  }
+  listLoader.loadData({
+    modelId: query?.modelId ? Number(query.modelId) : undefined, 
+    mainBodyColumnId: query?.mainBodyColumnId? Number(query.mainBodyColumnId) : undefined ,
+  })
+})
+</script>
+
+<style lang="scss">
+.article_list {
+  padding: 20rpx;
+}
+</style>

+ 11 - 0
src/pages/discover.vue

@@ -0,0 +1,11 @@
+<template>
+  <view class="d-flex flex-col">
+  </view>
+  <tabbar :current="1"></tabbar>
+</template>
+
+<script setup lang="ts">
+import Tabbar from '@/common/components/tabs/tabbar.vue';
+
+
+</script>

+ 70 - 0
src/pages/home.vue

@@ -0,0 +1,70 @@
+<template>
+  <view class="home d-flex flex-col bg-base">
+    <image 
+      class="w-100 position-absolute"
+      src="/static/images/home/BackgroundBanner.png"
+      mode="widthFix"
+    />
+    <image 
+      class="w-60 position-absolute title"
+      src="/static/images/home/Title.png"
+      mode="widthFix"
+    />
+    <view class="content d-flex flex-col wing-l">
+      <view class="shadow-lg radius-l bg-base p-3">
+        <image 
+          class="w-100"
+          src="/static/images/home/MainBanner.jpg"
+          mode="widthFix"
+        />
+        <view class="position-relative d-flex flex-row flex-wrap justify-between mt-3">
+          <view  
+            v-for="(tab, k) in subTabs" 
+            :key="k"
+            class="d-flex flex-column align-center width-1-5 mt-2"
+          >
+            <image class="width-100" :src="tab.icon" mode="widthFix" />
+            <text class="color-second-text mt-2">{{ tab.name }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+  <tabbar :current="0"></tabbar>
+</template>
+
+<script setup lang="ts">
+import Tabbar from '@/common/components/tabs/tabbar.vue';
+import MainBoxIcon1 from '/static/images/home/MainBoxIcon1.png';
+import MainBoxIcon2 from '/static/images/home/MainBoxIcon2.png';
+import MainBoxIcon3 from '/static/images/home/MainBoxIcon3.png';
+import MainBoxIcon4 from '/static/images/home/MainBoxIcon4.png';
+import MainBoxIcon5 from '/static/images/home/MainBoxIcon5.png';
+import MainBoxIcon6 from '/static/images/home/MainBoxIcon6.png';
+import MainBoxIcon7 from '/static/images/home/MainBoxIcon7.png';
+import MainBoxIcon8 from '/static/images/home/MainBoxIcon8.png';
+
+const subTabs = [
+  { name: '闽南语', icon: MainBoxIcon1 },
+  { name: '古早味', icon: MainBoxIcon2 },
+  { name: '先贤列传', icon: MainBoxIcon3 },
+  { name: '民俗活动', icon: MainBoxIcon4 },
+  { name: '红砖厝韵', icon: MainBoxIcon5 },
+  { name: '薪传匠艺', icon: MainBoxIcon6 },
+  { name: '工夫茶道', icon: MainBoxIcon7 },
+  { name: '闽南魂', icon: MainBoxIcon8 },
+]
+</script>
+
+<style lang="scss">
+.home {
+  .title {
+    left: 40rpx;
+    top: calc(var(--status-bar-height) + 50rpx);
+  }
+  .content {
+    margin-top: 30vh;
+    z-index: 1;
+  }
+}
+</style>

+ 11 - 0
src/pages/inhert.vue

@@ -0,0 +1,11 @@
+<template>
+  <view class="d-flex flex-col">
+  </view>
+  <tabbar :current="2"></tabbar>
+</template>
+
+<script setup lang="ts">
+import Tabbar from '@/common/components/tabs/tabbar.vue';
+
+
+</script>

+ 11 - 0
src/pages/travel.vue

@@ -0,0 +1,11 @@
+<template>
+  <view class="d-flex flex-col">
+  </view>
+  <tabbar :current="3"></tabbar>
+</template>
+
+<script setup lang="ts">
+import Tabbar from '@/common/components/tabs/tabbar.vue';
+
+
+</script>

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


部分文件因文件數量過多而無法顯示