Forráskód Böngészése

Merge branch 'release/3.3-202210210930'

gemercheung 2 éve
szülő
commit
14e2f7e272
100 módosított fájl, 8792 hozzáadás és 6145 törlés
  1. 37 0
      .cz-config.js
  2. 1 1
      .vscode/settings.json
  3. 50 0
      mock/notification/list.ts
  4. 78 70
      package.json
  5. 3461 3173
      pnpm-lock.yaml
  6. 3 0
      readme.md
  7. 12 7
      src/api/corporation/modal.ts
  8. 2 2
      src/api/corporation/model.ts
  9. 89 0
      src/api/dashboard/analysis.ts
  10. 23 0
      src/api/dashboard/model.ts
  11. 12 0
      src/api/member/list.ts
  12. 19 19
      src/api/member/model.ts
  13. 29 28
      src/api/model/baseModel.ts
  14. 78 0
      src/api/notification/list.ts
  15. 33 0
      src/api/notification/model.ts
  16. 3 2
      src/api/scene/list.ts
  17. 3 2
      src/api/scene/live.ts
  18. 101 95
      src/api/scene/model.ts
  19. 32 2
      src/api/staff/list.ts
  20. 5 0
      src/api/staff/model.ts
  21. 83 72
      src/api/sys/model/userModel.ts
  22. 51 1
      src/api/sys/user.ts
  23. 61 54
      src/components/CountDown/src/CountdownInput.vue
  24. 8 3
      src/components/Cropper/src/CopperModal.vue
  25. 2 0
      src/components/Form/index.ts
  26. 11 10
      src/components/Form/src/BasicForm.vue
  27. 4 0
      src/components/Form/src/componentMap.ts
  28. 28 3
      src/components/Form/src/components/ApiCascader.vue
  29. 4 11
      src/components/Form/src/components/ApiSelect.vue
  30. 135 0
      src/components/Form/src/components/ApiTransfer.vue
  31. 90 0
      src/components/Form/src/components/ApiTree.vue
  32. 19 7
      src/components/Form/src/components/FormItem.vue
  33. 6 1
      src/components/Form/src/hooks/useAdvanced.ts
  34. 52 2
      src/components/Form/src/hooks/useFormEvents.ts
  35. 47 4
      src/components/Form/src/hooks/useFormValues.ts
  36. 2 1
      src/components/Form/src/types/form.ts
  37. 3 2
      src/components/Form/src/types/index.ts
  38. 11 5
      src/components/Table/src/BasicTable.vue
  39. 2 0
      src/components/Table/src/componentMap.ts
  40. 1 1
      src/components/Table/src/components/HeaderCell.vue
  41. 1 1
      src/components/Table/src/components/TableImg.vue
  42. 103 60
      src/components/Table/src/components/editable/EditableCell.vue
  43. 1 1
      src/components/Table/src/components/editable/helper.ts
  44. 17 9
      src/components/Table/src/components/settings/ColumnSetting.vue
  45. 1 1
      src/components/Table/src/components/settings/SizeSetting.vue
  46. 1 1
      src/components/Table/src/components/settings/index.vue
  47. 11 11
      src/components/Table/src/hooks/useColumns.ts
  48. 1 1
      src/components/Table/src/hooks/useDataSource.ts
  49. 2 3
      src/components/Table/src/hooks/useTableFooter.ts
  50. 2 2
      src/components/Table/src/hooks/useTableScroll.ts
  51. 1 0
      src/components/Table/src/types/componentType.ts
  52. 17 1
      src/components/Table/src/types/pagination.ts
  53. 18 2
      src/components/Table/src/types/table.ts
  54. 4 0
      src/layouts/default/header/components/lock/LockModal.vue
  55. 60 20
      src/layouts/default/header/components/notify/index.vue
  56. 1 0
      src/layouts/default/header/components/user-dropdown/DropMenuItem.vue
  57. 2 2
      src/layouts/default/header/index.vue
  58. 1 1
      src/layouts/default/sider/MixSider.vue
  59. 9 5
      src/layouts/default/sider/useLayoutSider.ts
  60. 7 1
      src/layouts/default/tabs/components/TabContent.vue
  61. 25 0
      src/layouts/default/tabs/index.less
  62. 3 1
      src/layouts/page/index.vue
  63. 19 0
      src/locales/lang/zh-CN/antdLocale/DatePicker.ts
  64. 105 103
      src/locales/lang/zh-CN/sys.ts
  65. 11 10
      src/locales/lang/zh_CN.ts
  66. 69 69
      src/locales/useLocale.ts
  67. 78 69
      src/router/routes/index.ts
  68. 3 2
      src/settings/componentSetting.ts
  69. 29 29
      src/settings/localeSetting.ts
  70. 25 23
      src/utils/dateUtil.ts
  71. 287 275
      src/utils/http/axios/index.ts
  72. 131 130
      src/views/advertisement/list.vue
  73. 98 97
      src/views/advertisement/listDrawer.vue
  74. 147 146
      src/views/advertisement/pads.vue
  75. 106 106
      src/views/advertisement/padsDrawer.vue
  76. 1 1
      src/views/corporation/AddCorporationModal.vue
  77. 140 140
      src/views/corporation/chargeModal.vue
  78. 125 89
      src/views/dashboard/analysis/components/VisitAnalysis.vue
  79. 97 47
      src/views/dashboard/analysis/components/VisitAnalysisBar.vue
  80. 37 16
      src/views/dashboard/analysis/components/props.ts
  81. 278 0
      src/views/dashboard/analysis/enterprise.vue
  82. 274 25
      src/views/dashboard/analysis/index.vue
  83. 0 4
      src/views/feedback/list.vue
  84. 193 195
      src/views/member/list.vue
  85. 319 0
      src/views/notification/addModal.vue
  86. 169 0
      src/views/notification/center.vue
  87. 197 0
      src/views/notification/list.vue
  88. 5 0
      src/views/notification/template.vue
  89. 0 4
      src/views/order/list.vue
  90. 116 116
      src/views/product/addModal.vue
  91. 171 170
      src/views/product/category.vue
  92. 3 0
      src/views/product/drawer.data.ts
  93. 2 1
      src/views/product/goodsSpecs.vue
  94. 1 0
      src/views/product/list.data.ts
  95. 1 4
      src/views/product/list.vue
  96. 347 347
      src/views/product/productDrawer.vue
  97. 225 224
      src/views/product/ref.vue
  98. 1 1
      src/views/rightsEnterprises/BindModal.vue
  99. 3 1
      src/views/rightsEnterprises/addCameraModal.vue
  100. 0 0
      src/views/rightsEnterprises/camera.vue

+ 37 - 0
.cz-config.js

@@ -0,0 +1,37 @@
+module.exports = {
+  types: [
+    { value: 'feat', name: 'feat 🍄:    新增新的特性' },
+    { value: 'fix', name: 'fix 🐛:    修复 BUG' },
+    { value: 'docs', name: 'docs 📄:    修改文档、注释' },
+    {
+      value: 'refactor',
+      name: 'refactor 🎸:    代码重构,注意和特性、修复区分开',
+    },
+    { value: 'chore', name: 'chore 🧹:   构建过程或辅助工具的变动' },
+    { value: 'perf', name: 'perf ⚡:    提升性能' },
+    { value: 'test', name: 'test 👀:    添加一个测试' },
+    { value: 'tool', name: 'tool 🚗:    开发工具变动(构建、脚手架工具等)' },
+    { value: 'style', name: 'style ✂:    对代码格式的修改不影响逻辑' },
+    { value: 'revert', name: 'revert 🌝:     版本回滚' },
+    { value: 'update', name: 'update ⬆:    第三方库升级 ' },
+  ],
+
+  scopes: [{ name: '组件' }, { name: '样式' }, { name: '文档更改' }, { name: '其它变更' }],
+  allowTicketNumber: false,
+  isTicketNumberRequired: false,
+  ticketNumberPrefix: 'TICKET-',
+  ticketNumberRegExp: 'd{1,5}',
+  messages: {
+    type: '选择一种你的提交类型:',
+    scope: '选择一个scope (可选):',
+    customScope: 'Denote the SCOPE of this change:',
+    subject: '简要说明:\n',
+    body: '详细说明,使用"|"换行(可选):\n',
+    breaking: '非兼容性说明 (可选):\n',
+    footer: '关联关闭的issue,例如:#31, #34(可选):\n',
+    confirmCommit: '确定提交?',
+  },
+  allowCustomScopes: true,
+  allowBreakingChanges: ['新增', '修复'],
+  subjectLimit: 100,
+};

+ 1 - 1
.vscode/settings.json

@@ -96,7 +96,7 @@
     "editor.codeActionsOnSave": {
       "source.fixAll.eslint": false
     },
-    "editor.defaultFormatter": "octref.vetur"
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "i18n-ally.localesPaths": ["src/locales/lang"],
   "i18n-ally.keystyle": "nested",

+ 50 - 0
mock/notification/list.ts

@@ -0,0 +1,50 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { mock, Random } from 'mockjs';
+import { resultPageSuccess } from '../_util';
+Random.extend({
+  phone: function () {
+    const phonePrefixs = ['132', '135', '189']; // 自己写前缀哈
+    return this.pick(phonePrefixs) + mock(/\d{8}/); //Number()
+  },
+});
+// console.log(Random.phone());
+// 生成 1 - 10 个 随机手机号码
+
+const demoList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 200; index++) {
+    const { phone } = mock({
+      phone: '@phone',
+    });
+
+    result.push({
+      id: `${index}`,
+      name: '@cname',
+      nickName: '@cname',
+      avatarUrl: `@image('400x400', '@color', '微信头像')`,
+      'gender|1': [0, 1, 2],
+      city: '@city',
+      phone: phone,
+      province: '@province',
+      country: '@region',
+      language: '',
+      address: '@county(true)',
+      creatTime: '@datetime',
+      birthday: '@datetime',
+      lastLogin: '@datetime',
+    });
+  }
+  return result;
+})();
+
+export default [
+  {
+    url: '/zfb/notification/list',
+    timeout: 1000,
+    method: 'get',
+    response: ({ query }) => {
+      const { page = 1, pageSize = 20 } = query;
+      return resultPageSuccess(page, pageSize, demoList);
+    },
+  },
+] as MockMethod[];

+ 78 - 70
package.json

@@ -26,130 +26,138 @@
     "test:br": "npx http-server dist --cors --brotli -c-1",
     "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
     "prepare": "node -e \"if(require('fs').existsSync('.git')){process.exit(1)}\" || husky install",
-    "gen:icon": "esno ./build/generate/icon/index.ts"
+    "gen:icon": "esno ./build/generate/icon/index.ts",
+    "commit": "git cz"
   },
   "dependencies": {
     "@ant-design/colors": "^6.0.0",
-    "@ant-design/icons-vue": "^6.0.1",
-    "@iconify/iconify": "^2.1.0",
+    "@ant-design/icons-vue": "^6.1.0",
+    "@iconify/iconify": "^2.2.1",
     "@logicflow/core": "^0.7.16",
     "@logicflow/extension": "^0.7.16",
-    "@vue/runtime-core": "^3.2.26",
-    "@vue/shared": "^3.2.26",
-    "@vueuse/core": "^7.4.1",
-    "@vueuse/shared": "^7.4.1",
+    "@pansy/china-division": "^2.0.0",
+    "@vue/runtime-core": "^3.2.39",
+    "@vue/shared": "^3.2.39",
+    "@vueuse/core": "^7.7.1",
+    "@vueuse/shared": "^7.7.1",
     "@zxcvbn-ts/core": "^1.2.0",
-    "ant-design-vue": "3.0.0-beta.7",
+    "ant-design-vue": "^3.2.12",
     "axios": "^0.24.0",
-    "codemirror": "^5.65.0",
+    "codemirror": "^5.65.8",
     "cropperjs": "^1.5.12",
     "crypto-js": "^4.1.1",
-    "dayjs": "^1.10.7",
-    "echarts": "^5.2.2",
+    "dayjs": "^1.11.5",
+    "echarts": "^5.3.3",
     "intro.js": "^4.3.0",
     "js-base64": "^3.7.2",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
-    "moment": "^2.29.1",
+    "moment": "^2.29.4",
     "nprogress": "^0.2.0",
-    "path-to-regexp": "^6.2.0",
+    "path-to-regexp": "^6.2.1",
     "pinia": "2.0.9",
     "print-js": "^1.6.0",
-    "qrcode": "^1.5.0",
-    "qs": "^6.10.2",
+    "qrcode": "^1.5.1",
+    "qs": "^6.11.0",
     "resize-observer-polyfill": "^1.5.1",
     "showdown": "^1.9.1",
-    "sortablejs": "^1.14.0",
-    "tinymce": "^5.10.2",
-    "vditor": "^3.8.10",
-    "vue": "^3.2.26",
-    "vue-i18n": "^9.1.9",
-    "vue-json-pretty": "^1.8.2",
-    "vue-router": "^4.0.12",
-    "vue-types": "^4.1.1",
-    "xlsx": "^0.17.4"
+    "sortablejs": "^1.15.0",
+    "tinymce": "^5.10.5",
+    "vditor": "^3.8.17",
+    "vue": "^3.2.39",
+    "vue-i18n": "^9.2.2",
+    "vue-json-pretty": "^1.9.2",
+    "vue-router": "^4.1.5",
+    "vue-types": "^4.2.1",
+    "xlsx": "^0.17.5"
   },
   "devDependencies": {
-    "@commitlint/cli": "^16.0.1",
-    "@commitlint/config-conventional": "^16.0.0",
-    "@iconify/json": "^2.0.16",
+    "@commitlint/cli": "^16.3.0",
+    "@commitlint/config-conventional": "^16.2.4",
+    "@iconify/json": "^2.1.109",
     "@purge-icons/generated": "^0.7.0",
     "@types/codemirror": "^5.60.5",
-    "@types/crypto-js": "^4.0.2",
+    "@types/crypto-js": "^4.1.1",
     "@types/fs-extra": "^9.0.13",
-    "@types/inquirer": "^8.1.3",
+    "@types/inquirer": "^8.2.3",
     "@types/intro.js": "^3.0.2",
-    "@types/jest": "^27.0.3",
-    "@types/lodash-es": "^4.17.5",
-    "@types/mockjs": "^1.0.4",
-    "@types/node": "^17.0.5",
+    "@types/jest": "^27.5.2",
+    "@types/lodash-es": "^4.17.6",
+    "@types/mockjs": "^1.0.6",
+    "@types/node": "^17.0.45",
     "@types/nprogress": "^0.2.0",
-    "@types/qrcode": "^1.4.2",
+    "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
     "@types/showdown": "^1.9.4",
-    "@types/sortablejs": "^1.10.7",
-    "@typescript-eslint/eslint-plugin": "^5.8.1",
-    "@typescript-eslint/parser": "^5.8.1",
-    "@vitejs/plugin-legacy": "^1.6.4",
-    "@vitejs/plugin-vue": "^2.0.1",
-    "@vitejs/plugin-vue-jsx": "^1.3.3",
+    "@types/sortablejs": "^1.15.0",
+    "@typescript-eslint/eslint-plugin": "^5.37.0",
+    "@typescript-eslint/parser": "^5.37.0",
+    "@vitejs/plugin-legacy": "^1.8.2",
+    "@vitejs/plugin-vue": "^2.3.4",
+    "@vitejs/plugin-vue-jsx": "^1.3.10",
     "@vue/compiler-sfc": "3.2.26",
-    "@vue/test-utils": "^2.0.0-rc.18",
-    "autoprefixer": "^10.4.0",
-    "commitizen": "^4.2.4",
+    "@vue/test-utils": "^2.0.2",
+    "autoprefixer": "^10.4.11",
+    "commitizen": "^4.2.5",
     "conventional-changelog-cli": "^2.2.2",
     "cross-env": "^7.0.3",
+    "cz-customizable": "^7.0.0",
     "dotenv": "^10.0.0",
-    "eslint": "^8.5.0",
-    "eslint-config-prettier": "^8.3.0",
-    "eslint-define-config": "^1.2.1",
-    "eslint-plugin-jest": "^25.3.2",
-    "eslint-plugin-prettier": "^4.0.0",
-    "eslint-plugin-vue": "^8.2.0",
+    "eslint": "^8.23.1",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-define-config": "^1.7.0",
+    "eslint-plugin-jest": "^25.7.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-vue": "^8.7.1",
     "esno": "^0.13.0",
-    "fs-extra": "^10.0.0",
+    "fs-extra": "^10.1.0",
     "husky": "^7.0.4",
-    "inquirer": "^8.2.0",
-    "jest": "^27.4.5",
-    "less": "^4.1.2",
+    "inquirer": "^8.2.4",
+    "jest": "^27.5.1",
+    "less": "^4.1.3",
     "lint-staged": "12.1.4",
     "npm-run-all": "^4.1.5",
-    "postcss": "^8.4.5",
-    "postcss-html": "^1.3.0",
+    "postcss": "^8.4.16",
+    "postcss-html": "^1.5.0",
     "postcss-less": "^5.0.0",
-    "prettier": "^2.5.1",
+    "prettier": "^2.7.1",
     "rimraf": "^3.0.2",
-    "rollup-plugin-visualizer": "^5.5.2",
-    "stylelint": "^14.2.0",
-    "stylelint-config-html": "^1.0.0",
+    "rollup-plugin-visualizer": "^5.8.1",
+    "stylelint": "^14.12.0",
+    "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.3",
     "stylelint-config-recommended": "^6.0.0",
     "stylelint-config-standard": "^24.0.0",
     "stylelint-order": "^5.0.0",
-    "ts-jest": "^27.1.2",
-    "ts-node": "^10.4.0",
-    "typescript": "^4.5.4",
-    "vite": "^2.8.5",
+    "ts-jest": "^27.1.5",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.8.3",
+    "vite": "^2.9.15",
     "vite-plugin-compression": "^0.4.0",
     "vite-plugin-html": "^2.1.2",
-    "vite-plugin-imagemin": "^0.5.1",
+    "vite-plugin-imagemin": "^0.5.3",
     "vite-plugin-mock": "^2.9.6",
     "vite-plugin-purge-icons": "^0.7.0",
-    "vite-plugin-pwa": "^0.11.12",
+    "vite-plugin-pwa": "^0.11.13",
     "vite-plugin-rewrite-all": "^0.1.2",
     "vite-plugin-style-import": "^1.4.1",
-    "vite-plugin-svg-icons": "^1.0.5",
-    "vite-plugin-theme": "^0.8.1",
+    "vite-plugin-svg-icons": "^1.1.0",
+    "vite-plugin-theme": "^0.8.6",
     "vite-plugin-vue-setup-extend": "^0.3.0",
-    "vite-plugin-windicss": "^1.6.1",
-    "vue-eslint-parser": "^8.0.1",
-    "vue-tsc": "^0.30.1"
+    "vite-plugin-windicss": "^1.8.8",
+    "vue-eslint-parser": "^8.3.0",
+    "vue-tsc": "^0.30.6"
   },
   "resolutions": {
     "bin-wrapper": "npm:bin-wrapper-china",
     "rollup": "^2.56.3",
     "gifsicle": "5.2.0"
   },
+  "config": {
+    "commitizen": {
+      "path": "cz-customizable"
+    }
+  },
   "repository": {
     "type": "git",
     "url": "git+http://face3d.4dage.com:7005/zhangyupeng/zfb_mp.git "

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3461 - 3173
pnpm-lock.yaml


+ 3 - 0
readme.md

@@ -0,0 +1,3 @@
+# 2022-9-14 对接事项
+
+时间 统一为: YYYY-MM-DD HH:mm:ss, 不使用 UTF ease 标准时间。部分使用 timestamp

+ 12 - 7
src/api/corporation/modal.ts

@@ -74,14 +74,19 @@ export const selectUserList = (params: selectUserListParam) =>
   });
 
 export const checkUserAddAble = (params: checkUserParam) =>
-  defHttp.post<Result>({
-    url: Api.checkUserAddAble,
-    params,
-    headers: {
-      // @ts-ignore
-      ignoreCancelToken: true,
+  defHttp.post<Result>(
+    {
+      url: Api.checkUserAddAble,
+      params,
+      headers: {
+        // @ts-ignore
+        ignoreCancelToken: true,
+      },
     },
-  });
+    {
+      useResult: true,
+    },
+  );
 
 export const selectCompanyById = (params: selectCompanyParam) =>
   defHttp.post<Result>({

+ 2 - 2
src/api/corporation/model.ts

@@ -36,8 +36,8 @@ export interface auditParam {
 }
 
 export interface addPointParam {
-  id: number | null;
-  idpoint: number | null;
+  id: number | string;
+  point: number | string;
 }
 export interface selectUserListParam {
   id: number | null;

+ 89 - 0
src/api/dashboard/analysis.ts

@@ -0,0 +1,89 @@
+import { defHttp } from '/@/utils/http/axios';
+// PageParams
+import { BasicStaticsParams, UserStaticsModel, BulletChatStaticsModel } from './model';
+import { Result } from '/#/axios';
+
+enum Api {
+  bulletChat = '/zfb-api/zfb/platform/bulletChat/list',
+  userStatics = '/zfb-api/zfb/platform/dashboard/liveRoom/userStatics',
+  bulletChatStatics = '/zfb-api/zfb/platform/dashboard/liveRoom/bulletChatStatics',
+  liveRoomStatics = '/zfb-api/zfb/platform/dashboard/company/liveRoomStatics',
+  bulletChatExport = '/zfb-api/zfb/platform/bulletChat/export',
+  companyChatExport = '/zfb-api/zfb/platform/dashboard/company/export',
+}
+
+//留言列表
+
+export const bulletChatApi = (params: BasicStaticsParams) =>
+  defHttp.post<Result>({
+    url: Api.bulletChat,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+//企业看板
+
+export const liveRoomStaticsApi = (params: BasicStaticsParams) =>
+  defHttp.post<UserStaticsModel>({
+    url: Api.liveRoomStatics,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+//留言统计
+export const bulletChatStaticsApi = (params: BasicStaticsParams) =>
+  defHttp.post<BulletChatStaticsModel>({
+    url: Api.bulletChatStatics,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const userStaticsApi = (params: BasicStaticsParams) =>
+  defHttp.post<UserStaticsModel>({
+    url: Api.userStatics,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const bulletChatExportApi = (params: BasicStaticsParams) =>
+  defHttp.post<Blob>(
+    {
+      url: Api.bulletChatExport,
+      params,
+      responseType: 'blob',
+      headers: {
+        // @ts-ignore
+        ignoreCancelToken: true,
+      },
+    },
+    {
+      errorMessageMode: 'none',
+    },
+  );
+
+export const companyChatExportApi = (params: BasicStaticsParams) =>
+  defHttp.post<Blob>(
+    {
+      url: Api.companyChatExport,
+      params,
+      responseType: 'blob',
+      headers: {
+        // @ts-ignore
+        ignoreCancelToken: true,
+      },
+    },
+    {
+      errorMessageMode: 'none',
+    },
+  );

+ 23 - 0
src/api/dashboard/model.ts

@@ -0,0 +1,23 @@
+import { BasicPageParams } from '/@/api/model/baseModel';
+/**
+ * @description: Request list interface parameters
+ */
+export type PageParams = BasicPageParams;
+
+export interface BasicStaticsParams extends PageParams {
+  liveRoomId?: string;
+  time: string[];
+}
+export type StaticItemType = {
+  date: string;
+  amount: string | number;
+};
+export interface UserStaticsModel {
+  viewStatics: StaticItemType[];
+  shareStatics: StaticItemType[];
+}
+
+export interface BulletChatStaticsModel {
+  bulletChatAmounts: StaticItemType[];
+  userAmount: StaticItemType[];
+}

+ 12 - 0
src/api/member/list.ts

@@ -1,8 +1,10 @@
 import { defHttp } from '/@/utils/http/axios';
 import { PageParams, MemberListGetResultModel } from './model';
+import { Result } from '/#/axios';
 
 enum Api {
   pageList = '/zfb-api/zfb/user/list',
+  unbindWechat = '/zfb-api/zfb/user/unbindWechat',
 }
 
 /**
@@ -18,3 +20,13 @@ export const ListApi = (params: PageParams) =>
       ignoreCancelToken: true,
     },
   });
+
+export const unbindWechatApi = (id: number | string) =>
+  defHttp.post<Result>({
+    url: Api.unbindWechat,
+    params: { id },
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });

+ 19 - 19
src/api/member/model.ts

@@ -1,19 +1,19 @@
-import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
-/**
- * @description: Request list interface parameters
- */
-export type PageParams = BasicPageParams;
-
-export interface MemberListItem {
-  id: number;
-  name: string;
-  image: string;
-  link: string;
-  createTime: string;
-  isShow: boolean;
-}
-
-/**
- * @description: Request list return value
- */
-export type MemberListGetResultModel = BasicFetchResult<MemberListItem>;
+import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
+/**
+ * @description: Request list interface parameters
+ */
+export type PageParams = BasicPageParams;
+
+export interface MemberListItem {
+  id: number;
+  name: string;
+  image: string;
+  link: string;
+  createTime: string;
+  isShow: boolean;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type MemberListGetResultModel = BasicFetchResult<MemberListItem>;

+ 29 - 28
src/api/model/baseModel.ts

@@ -1,28 +1,29 @@
-export interface BasicPageParams {
-  page: number;
-  pageSize: number;
-}
-
-export interface BasicFetchResult<T> {
-  items: T[];
-  total: number;
-  endRow: number;
-  firstPage: number;
-  hasNextPage: boolean;
-  hasPreviousPage: boolean;
-  isFirstPage: boolean;
-  isLastPage: boolean;
-  lastPage: number;
-  list: T[];
-  navigateFirstPage: number;
-  navigateLastPage: number;
-  navigatePages: number;
-  navigatepageNums: number[];
-  nextPage: number;
-  pageNum: number;
-  pageSize: number;
-  pages: number;
-  prePage: number;
-  size: number;
-  startRow: number;
-}
+export interface BasicPageParams {
+  page: number;
+  pageSize?: number;
+  limit?: number;
+}
+
+export interface BasicFetchResult<T> {
+  items: T[];
+  total: number;
+  endRow: number;
+  firstPage: number;
+  hasNextPage: boolean;
+  hasPreviousPage: boolean;
+  isFirstPage: boolean;
+  isLastPage: boolean;
+  lastPage: number;
+  list: T[];
+  navigateFirstPage: number;
+  navigateLastPage: number;
+  navigatePages: number;
+  navigatepageNums: number[];
+  nextPage: number;
+  pageNum: number;
+  pageSize: number;
+  pages: number;
+  prePage: number;
+  size: number;
+  startRow: number;
+}

+ 78 - 0
src/api/notification/list.ts

@@ -0,0 +1,78 @@
+import { defHttp } from '/@/utils/http/axios';
+import {
+  PageParams,
+  ListGetResultModel,
+  NoticeParams,
+  UnreadModel,
+  DelNoticeParams,
+} from './model';
+import { Result } from '/#/axios';
+
+enum Api {
+  pageList = '/zfb-api/zfb/platform/notice/list',
+  addNotice = '/zfb-api/zfb/platform/notice/save',
+  delNotice = '/zfb-api/zfb/platform/notice/delete',
+  unreadNoticesList = '/zfb-api/zfb/platform/notice/unreadNoticesList',
+  readAll = '/zfb-api/zfb/platform/notice/readAll',
+}
+
+/**
+ * @description: Get sample list value
+ */
+
+export const listApi = (params: PageParams) =>
+  defHttp.post<ListGetResultModel>({
+    url: Api.pageList,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const addNoticeApi = (params: NoticeParams) =>
+  defHttp.post<Result>({
+    url: Api.addNotice,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const editNoticeApi = (params: NoticeParams) =>
+  defHttp.post<Result>({
+    url: Api.addNotice,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const deleteNoticeApi = (params: DelNoticeParams) =>
+  defHttp.post<Result>({
+    url: Api.delNotice,
+    params,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+
+export const unreadNoticeApi = () =>
+  defHttp.post<UnreadModel>({
+    url: Api.unreadNoticesList,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });
+export const readAllApi = () =>
+  defHttp.post<Result>({
+    url: Api.readAll,
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });

+ 33 - 0
src/api/notification/model.ts

@@ -0,0 +1,33 @@
+import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
+/**
+ * @description: Request list interface parameters
+ */
+export type PageParams = BasicPageParams;
+
+export interface MemberListItem {
+  id: number;
+  content: string;
+  type: string;
+  effectiveTime: string[];
+  createTime: string;
+}
+
+export interface NoticeParams {
+  id?: string;
+  title: string;
+  content: string;
+  type: string;
+  effectiveTime: string[];
+}
+export interface DelNoticeParams {
+  id: string;
+}
+
+export interface UnreadModel {
+  list: any[];
+  totalCount: number;
+}
+/**
+ * @description: Request list return value
+ */
+export type ListGetResultModel = BasicFetchResult<MemberListItem>;

+ 3 - 2
src/api/scene/list.ts

@@ -1,6 +1,7 @@
 import { defHttp } from '/@/utils/http/axios';
 import {
-  PageParams,
+  // PageParams,
+  ListParams,
   RentListGetResultModel,
   SceneEditParam,
   SceneDownloadParam,
@@ -86,7 +87,7 @@ export const listRoomsApi = (params: UpdateParams) =>
       ignoreCancelToken: true,
     },
   });
-export const ListApi = (params: PageParams) =>
+export const ListApi = (params: ListParams) =>
   defHttp.post<RentListGetResultModel>({
     url: Api.pageList,
     params,

+ 3 - 2
src/api/scene/live.ts

@@ -1,6 +1,7 @@
 import { defHttp } from '/@/utils/http/axios';
 import {
   PageParams,
+  ListParams,
   InfoParams,
   RentListGetResultModel,
   SceneLiveItem,
@@ -31,7 +32,7 @@ export type SceneLiveItemResult = SceneLiveItem;
  * @description: Get sample list value
  */
 
-export const ListApi = (params: PageParams) =>
+export const ListApi = (params: ListParams) =>
   defHttp.get<RentListGetResultModel>({
     url: Api.pageList,
     params,
@@ -106,7 +107,7 @@ export function uploadLiveVideoApi(
   );
 }
 
-export const getAllSceneApi = (params: PageParams) =>
+export const getAllSceneApi = (params: ListParams) =>
   defHttp.post<RentListGetResultModel>({
     url: Api.getAllScene,
     params,

+ 101 - 95
src/api/scene/model.ts

@@ -1,95 +1,101 @@
-import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
-/**
- * @description: Request list interface parameters
- */
-export type PageParams = BasicPageParams;
-export interface bindAnchorListParam {
-  brandId?: number | string;
-  canShow?: number;
-  type?: number;
-  userId?: number | string;
-}
-export interface InfoParams {
-  id?: number | string;
-  token?: string;
-}
-export interface UpdateParams {
-  sceneNum?: string;
-  isShow?: string;
-  appListPicUrl?: string;
-}
-
-export interface SceneDownloadParam {
-  sceneNum?: string;
-}
-export interface SceneEditParam {
-  sceneNum: string;
-  userName: string;
-}
-
-export interface SceneLiveItem {
-  address: string;
-  adminId: number;
-  appListPicUrl: string;
-  bindShowerId: number;
-  bindShowerName: string;
-  bindShowerNameList: string;
-  city: string;
-  contractPhone: string;
-  createTime: number;
-  createUserDeptId: number;
-  createUserId: number;
-  deleted: number;
-  id: number;
-  introduceVideo: string;
-  introduceVideoCover: string;
-  latitude: number;
-  liveRoomUrl: string;
-  livestreamStatus: number;
-  longitude: number;
-  name: string;
-  picList: string;
-  sceneName: string;
-  sceneNum: string;
-  sceneUrl: string;
-  shareWxQrCode: string;
-  simpleDesc: string;
-  sortOrder: number;
-  token: string;
-  type: number;
-  updateTime: number;
-  updateUserId: number;
-}
-
-export interface SceneProccessItem {
-  precent?: number;
-  status?: number;
-  url?: string;
-}
-
-export interface sceneItem {
-  appListPicUrl: string;
-  createTime: number;
-  createUserId: number;
-  deleted: number;
-  id: number;
-  isShow: number;
-  name: string;
-  sceneNum: string;
-  sceneUrl: string;
-  token: string;
-  updateTime: number;
-  updateUserId: number;
-}
-
-export interface SceneLiveModel {
-  brand: SceneLiveItem;
-  code?: number;
-}
-/**
- * @description: Request list return value
- */
-export type RentListGetResultModel = BasicFetchResult<SceneLiveItem>;
-export type LiveListGetResultModel = BasicFetchResult<SceneLiveItem>;
-export type GetDownloadProcessModel = BasicFetchResult<SceneProccessItem>;
-export type GetAllSceneModel = BasicFetchResult<sceneItem>;
+import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
+/**
+ * @description: Request list interface parameters
+ */
+export type PageParams = BasicPageParams;
+
+export interface ListParams extends PageParams {
+  sceneName?: string;
+  sceneNum?: string;
+}
+export interface bindAnchorListParam {
+  brandId?: number | string;
+  canShow?: number;
+  type?: number;
+  userId?: number | string;
+}
+export interface InfoParams {
+  id?: number | string;
+  token?: string;
+}
+export interface UpdateParams {
+  sceneNum?: string;
+  isShow?: string;
+  appListPicUrl?: string;
+}
+
+export interface SceneDownloadParam {
+  sceneNum?: string;
+}
+export interface SceneEditParam {
+  sceneNum: string;
+  userName: string;
+}
+
+export interface SceneLiveItem {
+  address: string;
+  adminId: number;
+  appListPicUrl: string;
+  bindShowerId: number;
+  bindShowerName: string;
+  bindShowerNameList: string;
+  city: string;
+  contractPhone: string;
+  createTime: number;
+  createUserDeptId: number;
+  createUserId: number;
+  deleted: number;
+  id: number;
+  introduceVideo: string;
+  introduceVideoCover: string;
+  latitude: number;
+  liveRoomUrl: string;
+  livestreamStatus: number;
+  longitude: number;
+  name: string;
+  picList: string;
+  sceneName: string;
+  sceneNum: string;
+  sceneUrl: string;
+  shareWxQrCode: string;
+  simpleDesc: string;
+  sortOrder: number;
+  token: string;
+  type: number;
+  updateTime: number;
+  updateUserId: number;
+  contactId: number;
+}
+
+export interface SceneProccessItem {
+  precent?: number;
+  status?: number;
+  url?: string;
+}
+
+export interface sceneItem {
+  appListPicUrl: string;
+  createTime: number;
+  createUserId: number;
+  deleted: number;
+  id: number;
+  isShow: number;
+  name: string;
+  sceneNum: string;
+  sceneUrl: string;
+  token: string;
+  updateTime: number;
+  updateUserId: number;
+}
+
+export interface SceneLiveModel {
+  brand: SceneLiveItem;
+  code?: number;
+}
+/**
+ * @description: Request list return value
+ */
+export type RentListGetResultModel = BasicFetchResult<SceneLiveItem>;
+export type LiveListGetResultModel = BasicFetchResult<SceneLiveItem>;
+export type GetDownloadProcessModel = BasicFetchResult<SceneProccessItem>;
+export type GetAllSceneModel = BasicFetchResult<sceneItem>;

+ 32 - 2
src/api/staff/list.ts

@@ -1,6 +1,6 @@
 import { defHttp } from '/@/utils/http/axios';
-import { PageParams, ListGetResultModel, DelParams, roleParams } from './model';
-import { Result } from '/#/axios';
+import { PageParams, ListGetResultModel, DelParams, roleParams, ExportParam } from './model';
+import { Result, UploadFileParams } from '/#/axios';
 
 enum Api {
   pageList = '/zfb-api/zfb/shop/sys/user/staffList',
@@ -13,6 +13,8 @@ enum Api {
   deleteStaff = '/zfb-api/zfb/shop/sys/user/deleteStaff',
   getNumByStaff = '/zfb-api/zfb/shop/sys/user/getNumByStaff',
   clean = '/zfb-api/zfb/loginOutByUser',
+  upload = '/zfb-api/zfb/sys/oss/upload',
+  staffExport = '/zfb-api/zfb/shop/sys/user/staffExport',
 }
 
 /**
@@ -126,3 +128,31 @@ export const updateApi = (params) =>
       ignoreCancelToken: true,
     },
   });
+export function uploadApi(
+  params: UploadFileParams,
+  // onUploadProgress: (progressEvent: ProgressEvent) => void,
+) {
+  return defHttp.uploadFile<Result>(
+    {
+      url: Api.upload,
+      // onUploadProgress,
+    },
+    params,
+  );
+}
+
+export const exportApi = (params: ExportParam) =>
+  defHttp.post<Blob>(
+    {
+      url: Api.staffExport,
+      params,
+      responseType: 'blob',
+      headers: {
+        // @ts-ignore
+        ignoreCancelToken: true,
+      },
+    },
+    {
+      errorMessageMode: 'none',
+    },
+  );

+ 5 - 0
src/api/staff/model.ts

@@ -4,6 +4,11 @@ import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
  */
 export type PageParams = BasicPageParams;
 
+export interface ExportParam extends PageParams {
+  staffName?: string;
+  staffPhone: string;
+}
+
 export interface DelParams {
   userId: number;
   toUserId: number;

+ 83 - 72
src/api/sys/model/userModel.ts

@@ -1,72 +1,83 @@
-import type { UserInfo } from '/#/store';
-/**
- * @description: Login interface parameters
- */
-export interface LoginParams {
-  userName: string;
-  userPassword: string;
-  captcha: string;
-}
-
-export interface RoleInfo {
-  userID?: string;
-  roleName: string;
-  value: string;
-  brandList: string[] | any;
-  canShow: string;
-  createTime: number;
-  createUserId: number;
-  deptExpirationDate: number;
-  deptId: number;
-  deptManagerPhoneNum: string;
-  deptName: string;
-  email: string;
-  fdkkPassword: string;
-  fdkkUser: string;
-  isPlatformStreamer: false;
-  mobile: string;
-  parentDeptId: number;
-  parentDeptName: number;
-  password: string;
-  roleId: number;
-  roleIdList: any[];
-  roleList: number;
-  status: number;
-  userId: number;
-  username: string;
-}
-
-/**
- * @description: Login interface return value
- */
-export interface LoginResultModel {
-  id?: string | number;
-  token: string;
-  role?: RoleInfo;
-  user: RoleInfo;
-}
-export type GetUserInfoModel = UserInfo;
-
-export interface updateUserInfoPasswordParam {
-  id: string | number;
-  password?: string;
-  newPassword?: string;
-}
-/**
- * @description: Get user information return value
- */
-// export interface GetUserInfoModel {
-//   roles: RoleInfo[];
-//   // 用户id
-//   id: string | number;
-
-//   // userId: string | number;
-//   // 用户名
-//   userName: string;
-//   // 真实名字
-//   realName: string;
-//   // 头像
-//   avatar: string;
-//   // 介绍
-//   desc?: string;
-// }
+import type { UserInfo } from '/#/store';
+/**
+ * @description: Login interface parameters
+ */
+export interface LoginParams {
+  userName: string;
+  userPassword: string;
+  captcha: string;
+}
+
+export interface SendcodeParams {
+  phone: string;
+  areaNum?: string;
+}
+
+export interface RegisterParams {
+  userName: string;
+  userPassword: string;
+  area: string;
+  code: string;
+}
+export interface RoleInfo {
+  userID?: string;
+  roleName: string;
+  value: string;
+  brandList: string[] | any;
+  canShow: string;
+  createTime: number;
+  createUserId: number;
+  deptExpirationDate: number;
+  deptId: number;
+  deptManagerPhoneNum: string;
+  deptName: string;
+  email: string;
+  fdkkPassword: string;
+  fdkkUser: string;
+  isPlatformStreamer: false;
+  mobile: string;
+  parentDeptId: number;
+  parentDeptName: number;
+  password: string;
+  roleId: number;
+  roleIdList: any[];
+  roleList: number;
+  status: number;
+  userId: number;
+  username: string;
+}
+
+/**
+ * @description: Login interface return value
+ */
+export interface LoginResultModel {
+  id?: string | number;
+  token: string;
+  role?: RoleInfo;
+  user: RoleInfo;
+}
+export type GetUserInfoModel = UserInfo;
+
+export interface updateUserInfoPasswordParam {
+  id: string | number;
+  password?: string;
+  newPassword?: string;
+}
+/**
+ * @description: Get user information return value
+ */
+// export interface GetUserInfoModel {
+//   roles: RoleInfo[];
+//   // 用户id
+//   id: string | number;
+
+//   // userId: string | number;
+//   // 用户名
+//   userName: string;
+//   // 真实名字
+//   realName: string;
+//   // 头像
+//   avatar: string;
+//   // 介绍
+//   desc?: string;
+// }

+ 51 - 1
src/api/sys/user.ts

@@ -1,6 +1,8 @@
 import { defHttp } from '/@/utils/http/axios';
 import {
   LoginParams,
+  SendcodeParams,
+  RegisterParams,
   LoginResultModel,
   GetUserInfoModel,
   updateUserInfoPasswordParam,
@@ -8,12 +10,14 @@ import {
 import { encodeStr } from '/@/utils/encodeUtil';
 import { ErrorMessageMode } from '/#/axios';
 import { ContentTypeEnum } from '/@/enums/httpEnum';
-
+import type { Result } from '/#/axios';
 // import { encode } from 'js-base64';
 
 enum Api {
   // Login = '/zfb-api/zfb/sys/login',
   Login = '/zfb-api/zfb/loginBackground',
+  Sendcode = '/zfb-api/zfb/phone/getSMForApp',
+  Register = '/zfb-api/zfb/platform/user/register',
   Logout = '/logout',
   updatePassword = '/zfb-api/zfb/user/updatePassword',
   GetUserInfo = '/zfb-api/zfb/shop/sys/user/infoAnon',
@@ -48,6 +52,52 @@ export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal')
   );
 }
 
+export function registerApi(params: RegisterParams) {
+  const paramData: RegisterParams = {
+    area: '86',
+    code: params.code,
+    userName: params.userName,
+    userPassword: encodeStr(window.btoa(params.userPassword)),
+  };
+
+  return defHttp.post<Result>(
+    {
+      url: Api.Register,
+      // params,
+      params: paramData,
+      headers: { 'Content-Type': ContentTypeEnum.JSON },
+    },
+    {
+      errorMessageMode: 'none',
+      useResult: true,
+    },
+  );
+}
+
+export async function sendCodeApi(params: SendcodeParams) {
+  const paramData: SendcodeParams = {
+    areaNum: '86',
+    phone: params.phone,
+  };
+  const res = await defHttp.post<Result>(
+    {
+      url: Api.Sendcode,
+      // params,
+      params: paramData,
+      headers: { 'Content-Type': ContentTypeEnum.JSON },
+    },
+    {
+      useResult: true,
+    },
+  );
+
+  if (res.code === 200) {
+    return Promise.resolve(true);
+  } else {
+    return Promise.resolve(false);
+  }
+}
+
 /**
  * @description: getUserInfo
  */

+ 61 - 54
src/components/CountDown/src/CountdownInput.vue

@@ -1,54 +1,61 @@
-<template>
-  <a-input v-bind="$attrs" :class="prefixCls" :size="size" :value="state">
-    <template #addonAfter>
-      <CountButton :size="size" :count="count" :value="state" :beforeStartFunc="sendCodeApi" />
-    </template>
-    <template #[item]="data" v-for="item in Object.keys($slots).filter((k) => k !== 'addonAfter')">
-      <slot :name="item" v-bind="data || {}"></slot>
-    </template>
-  </a-input>
-</template>
-<script lang="ts">
-  import { defineComponent, PropType } from 'vue';
-  import CountButton from './CountButton.vue';
-  import { useDesign } from '/@/hooks/web/useDesign';
-  import { useRuleFormItem } from '/@/hooks/component/useFormItem';
-
-  const props = {
-    value: { type: String },
-    size: { type: String, validator: (v) => ['default', 'large', 'small'].includes(v) },
-    count: { type: Number, default: 60 },
-    sendCodeApi: {
-      type: Function as PropType<() => Promise<boolean>>,
-      default: null,
-    },
-  };
-
-  export default defineComponent({
-    name: 'CountDownInput',
-    components: { CountButton },
-    inheritAttrs: false,
-    props,
-    setup(props) {
-      const { prefixCls } = useDesign('countdown-input');
-      const [state] = useRuleFormItem(props);
-
-      return { prefixCls, state };
-    },
-  });
-</script>
-<style lang="less">
-  @prefix-cls: ~'@{namespace}-countdown-input';
-
-  .@{prefix-cls} {
-    .ant-input-group-addon {
-      padding-right: 0;
-      background-color: transparent;
-      border: none;
-
-      button {
-        font-size: 14px;
-      }
-    }
-  }
-</style>
+<template>
+  <a-input v-bind="$attrs" :class="prefixCls" :size="size" :value="state">
+    <template #addonAfter>
+      <CountButton
+        :disabled="buttonDisabled"
+        :size="size"
+        :count="count"
+        :value="state"
+        :beforeStartFunc="sendCodeApi"
+      />
+    </template>
+    <template #[item]="data" v-for="item in Object.keys($slots).filter((k) => k !== 'addonAfter')">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+  </a-input>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+  import CountButton from './CountButton.vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useRuleFormItem } from '/@/hooks/component/useFormItem';
+
+  const props = {
+    value: { type: String },
+    buttonDisabled: { type: Boolean, default: false },
+    size: { type: String, validator: (v) => ['default', 'large', 'small'].includes(v) },
+    count: { type: Number, default: 60 },
+    sendCodeApi: {
+      type: Function as PropType<() => Promise<boolean>>,
+      default: null,
+    },
+  };
+
+  export default defineComponent({
+    name: 'CountDownInput',
+    components: { CountButton },
+    inheritAttrs: false,
+    props,
+    setup(props) {
+      const { prefixCls } = useDesign('countdown-input');
+      const [state] = useRuleFormItem(props);
+
+      return { prefixCls, state };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-countdown-input';
+
+  .@{prefix-cls} {
+    .ant-input-group-addon {
+      padding-right: 0;
+      background-color: transparent;
+      border: none;
+
+      button {
+        font-size: 14px;
+      }
+    }
+  }
+</style>

+ 8 - 3
src/components/Cropper/src/CopperModal.vue

@@ -6,6 +6,7 @@
     width="800px"
     :canFullscreen="false"
     @ok="handleOk"
+    @cancel="handleCancel"
     :okText="t('component.cropper.okText')"
   >
     <div :class="prefixCls">
@@ -156,9 +157,7 @@
       const { t } = useI18n();
 
       watchEffect(() => {
-        if (props.src) {
-          src.value = props.src;
-        }
+        src.value = props.src;
       });
 
       // Block upload
@@ -226,6 +225,11 @@
           }
         }
       }
+      function handleCancel() {
+        // debugger;
+        src.value = '';
+        previewSource.value = '';
+      }
 
       return {
         t,
@@ -238,6 +242,7 @@
         handleReady,
         handlerToolbar,
         handleOk,
+        handleCancel,
       };
     },
   });

+ 2 - 0
src/components/Form/index.ts

@@ -9,7 +9,9 @@ export { useForm } from './src/hooks/useForm';
 export { default as ApiSelect } from './src/components/ApiSelect.vue';
 export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
+export { default as ApiTree } from './src/components/ApiTree.vue';
 export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
 export { default as ApiCascader } from './src/components/ApiCascader.vue';
+export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
 
 export { BasicForm };

+ 11 - 10
src/components/Form/src/BasicForm.vue

@@ -62,12 +62,13 @@
 
   import { basicProps } from './props';
   import { useDesign } from '/@/hooks/web/useDesign';
+  import { cloneDeep } from 'lodash-es';
 
   export default defineComponent({
     name: 'BasicForm',
     components: { FormItem, Form, Row, FormAction },
     props: basicProps,
-    emits: ['advanced-change', 'reset', 'submit', 'register'],
+    emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
     setup(props, { emit, attrs }) {
       const formModel = reactive<Recordable>({});
       const modalFn = useModalContext();
@@ -132,9 +133,11 @@
           }
         }
         if (unref(getProps).showAdvancedButton) {
-          return schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[];
+          return cloneDeep(
+            schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[],
+          );
         } else {
-          return schemas as FormSchema[];
+          return cloneDeep(schemas as FormSchema[]);
         }
       });
 
@@ -240,13 +243,11 @@
 
       function setFormModel(key: string, value: any) {
         formModel[key] = value;
-        // const { validateTrigger } = unref(getBindValue);
-        // console.log('key', key, schemas[3]);
-
-        // console.log('validateTrigger', validateTrigger, formModel);
-        // if (!validateTrigger || validateTrigger === 'change') {
-        //   validateFields([key]).catch((_) => {});
-        // }
+        const { validateTrigger } = unref(getBindValue);
+        if (!validateTrigger || validateTrigger === 'change') {
+          validateFields([key]).catch((_) => {});
+        }
+        emit('field-value-change', key, value);
       }
 
       function handleEnterPress(e: KeyboardEvent) {

+ 4 - 0
src/components/Form/src/componentMap.ts

@@ -24,8 +24,10 @@ import {
 import ApiRadioGroup from './components/ApiRadioGroup.vue';
 import RadioButtonGroup from './components/RadioButtonGroup.vue';
 import ApiSelect from './components/ApiSelect.vue';
+import ApiTree from './components/ApiTree.vue';
 import ApiTreeSelect from './components/ApiTreeSelect.vue';
 import ApiCascader from './components/ApiCascader.vue';
+import ApiTransfer from './components/ApiTransfer.vue';
 import { BasicUpload } from '/@/components/Upload';
 import { StrengthMeter } from '/@/components/StrengthMeter';
 import { IconPicker } from '/@/components/Icon';
@@ -43,6 +45,7 @@ componentMap.set('AutoComplete', AutoComplete);
 
 componentMap.set('Select', Select);
 componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('ApiTree', ApiTree);
 componentMap.set('TreeSelect', TreeSelect);
 componentMap.set('ApiTreeSelect', ApiTreeSelect);
 componentMap.set('ApiRadioGroup', ApiRadioGroup);
@@ -55,6 +58,7 @@ componentMap.set('ApiCascader', ApiCascader);
 componentMap.set('Cascader', Cascader);
 componentMap.set('Slider', Slider);
 componentMap.set('Rate', Rate);
+componentMap.set('ApiTransfer', ApiTransfer);
 
 componentMap.set('DatePicker', DatePicker);
 componentMap.set('MonthPicker', DatePicker.MonthPicker);

+ 28 - 3
src/components/Form/src/components/ApiCascader.vue

@@ -26,7 +26,7 @@
   import { get, omit } from 'lodash-es';
   import { useRuleFormItem } from '/@/hooks/component/useFormItem';
   import { LoadingOutlined } from '@ant-design/icons-vue';
-
+  import { useI18n } from '/@/hooks/web/useI18n';
   interface Option {
     value: string;
     label: string;
@@ -76,7 +76,7 @@
       const loading = ref<boolean>(false);
       const emitData = ref<any[]>([]);
       const isFirstLoad = ref(true);
-
+      const { t } = useI18n();
       // Embedded in the form, just use the hook binding to perform form verification
       const [state] = useRuleFormItem(props, 'value', 'change', emitData);
 
@@ -131,6 +131,28 @@
         }
       }
 
+      const deepSearch = (
+        data: Option[],
+        value: string,
+        key = 'title',
+        sub = 'children',
+        tempObj = {} as any,
+      ) => {
+        if (value && data) {
+          data.find((node) => {
+            if (node[key] == value) {
+              tempObj.found = node;
+              return node;
+            }
+            return deepSearch(node[sub], value, key, sub, tempObj);
+          });
+          if (tempObj.found) {
+            return tempObj.found;
+          }
+        }
+        return false;
+      };
+
       async function loadData(selectedOptions: Option[]) {
         const targetOption = selectedOptions[selectedOptions.length - 1];
         targetOption.loading = true;
@@ -143,7 +165,9 @@
           });
           if (Array.isArray(res)) {
             const children = generatorOptions(res);
-            targetOption.children = children;
+            const targetChildren = deepSearch(children, targetOption.value, 'value');
+            // console.log('targetChildren', targetChildren.children);
+            targetOption.children = targetChildren.children;
             return;
           }
           if (props.resultField) {
@@ -188,6 +212,7 @@
         state,
         options,
         loading,
+        t,
         handleChange,
         loadData,
         handleRenderDisplay,

+ 4 - 11
src/components/Form/src/components/ApiSelect.vue

@@ -2,9 +2,7 @@
   <Select
     @dropdown-visible-change="handleFetch"
     v-bind="$attrs"
-    show-search
     @change="handleChange"
-    :filter-option="filterOption"
     :options="getOptions"
     v-model:value="state"
   >
@@ -69,18 +67,13 @@
       const emitData = ref<any[]>([]);
       const attrs = useAttrs();
       const { t } = useI18n();
-      const filterOption = (input, option) => {
-        try {
-          return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
-        } catch (error) {
-          return false;
-        }
-      };
+
       // Embedded in the form, just use the hook binding to perform form verification
       const [state] = useRuleFormItem(props, 'value', 'change', emitData);
 
       const getOptions = computed(() => {
         const { labelField, valueField, numberToString } = props;
+
         return unref(options).reduce((prev, next: Recordable) => {
           if (next) {
             const value = next[valueField];
@@ -123,7 +116,7 @@
           }
           emitChange();
         } catch (error) {
-          console.warn('error', error);
+          console.warn(error);
         } finally {
           loading.value = false;
         }
@@ -148,7 +141,7 @@
         emitData.value = args;
       }
 
-      return { state, attrs, getOptions, loading, t, handleFetch, handleChange, filterOption };
+      return { state, attrs, getOptions, loading, t, handleFetch, handleChange };
     },
   });
 </script>

+ 135 - 0
src/components/Form/src/components/ApiTransfer.vue

@@ -0,0 +1,135 @@
+<template>
+  <Transfer
+    :data-source="getdataSource"
+    show-search
+    :filter-option="filterOption"
+    :render="(item) => item.title"
+    :showSelectAll="showSelectAll"
+    :selectedKeys="selectedKeys"
+    :targetKeys="getTargetKeys"
+    :showSearch="showSearch"
+    @change="handleChange"
+  />
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue';
+  import { Transfer } from 'ant-design-vue';
+  import { isFunction } from '/@/utils/is';
+  import { get, omit } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer';
+  export default defineComponent({
+    name: 'ApiTransfer',
+    components: { Transfer },
+    props: {
+      value: { type: Array<string> },
+      api: {
+        type: Function as PropType<(arg?: Recordable) => Promise<TransferItem[]>>,
+        default: null,
+      },
+      params: { type: Object },
+      dataSource: { type: Array<TransferItem> },
+      immediate: propTypes.bool.def(true),
+      alwaysLoad: propTypes.bool.def(false),
+      afterFetch: { type: Function as PropType<Fn> },
+      resultField: propTypes.string.def(''),
+      labelField: propTypes.string.def('title'),
+      valueField: propTypes.string.def('key'),
+      showSearch: { type: Boolean, default: false },
+      disabled: { type: Boolean, default: false },
+      filterOption: {
+        type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
+      },
+      selectedKeys: { type: Array<string> },
+      showSelectAll: { type: Boolean, default: false },
+      targetKeys: { type: Array<string> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const _dataSource = ref<TransferItem[]>([]);
+      const _targetKeys = ref<string[]>([]);
+      const { t } = useI18n();
+
+      const getAttrs = computed(() => {
+        return {
+          ...(!props.api ? { dataSource: unref(_dataSource) } : {}),
+          ...attrs,
+        };
+      });
+      const getdataSource = computed(() => {
+        const { labelField, valueField } = props;
+
+        return unref(_dataSource).reduce((prev, next: Recordable) => {
+          if (next) {
+            prev.push({
+              ...omit(next, [labelField, valueField]),
+              title: next[labelField],
+              key: next[valueField],
+            });
+          }
+          return prev;
+        }, [] as TransferItem[]);
+      });
+      const getTargetKeys = computed<string[]>(() => {
+        if (unref(_targetKeys).length > 0) {
+          return unref(_targetKeys);
+        }
+        if (Array.isArray(props.value)) {
+          return props.value;
+        }
+        return [];
+      });
+
+      function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
+        _targetKeys.value = keys;
+        console.log(direction);
+        console.log(moveKeys);
+        emit('change', keys);
+      }
+
+      watchEffect(() => {
+        props.immediate && !props.alwaysLoad && fetch();
+      });
+
+      watch(
+        () => props.params,
+        () => {
+          fetch();
+        },
+        { deep: true },
+      );
+
+      async function fetch() {
+        const api = props.api;
+        if (!api || !isFunction(api)) {
+          if (Array.isArray(props.dataSource)) {
+            _dataSource.value = props.dataSource;
+          }
+          return;
+        }
+        _dataSource.value = [];
+        try {
+          const res = await api(props.params);
+          if (Array.isArray(res)) {
+            _dataSource.value = res;
+            emitChange();
+            return;
+          }
+          if (props.resultField) {
+            _dataSource.value = get(res, props.resultField) || [];
+          }
+          emitChange();
+        } catch (error) {
+          console.warn(error);
+        } finally {
+        }
+      }
+      function emitChange() {
+        emit('options-change', unref(getdataSource));
+      }
+      return { getTargetKeys, getdataSource, t, getAttrs, handleChange };
+    },
+  });
+</script>

+ 90 - 0
src/components/Form/src/components/ApiTree.vue

@@ -0,0 +1,90 @@
+<template>
+  <a-tree v-bind="getAttrs" @change="handleChange">
+    <template #[item]="data" v-for="item in Object.keys($slots)">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+    <template #suffixIcon v-if="loading">
+      <LoadingOutlined spin />
+    </template>
+  </a-tree>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
+  import { Tree } from 'ant-design-vue';
+  import { isArray, isFunction } from '/@/utils/is';
+  import { get } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { LoadingOutlined } from '@ant-design/icons-vue';
+  export default defineComponent({
+    name: 'ApiTree',
+    components: { ATree: Tree, LoadingOutlined },
+    props: {
+      api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
+      params: { type: Object },
+      immediate: { type: Boolean, default: true },
+      resultField: propTypes.string.def(''),
+      afterFetch: { type: Function as PropType<Fn> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const treeData = ref<Recordable[]>([]);
+      const isFirstLoaded = ref<Boolean>(false);
+      const loading = ref(false);
+      const getAttrs = computed(() => {
+        return {
+          ...(props.api ? { treeData: unref(treeData) } : {}),
+          ...attrs,
+        };
+      });
+
+      function handleChange(...args) {
+        emit('change', ...args);
+      }
+
+      watch(
+        () => props.params,
+        () => {
+          !unref(isFirstLoaded) && fetch();
+        },
+        { deep: true },
+      );
+
+      watch(
+        () => props.immediate,
+        (v) => {
+          v && !isFirstLoaded.value && fetch();
+        },
+      );
+
+      onMounted(() => {
+        props.immediate && fetch();
+      });
+
+      async function fetch() {
+        const { api, afterFetch } = props;
+        if (!api || !isFunction(api)) return;
+        loading.value = true;
+        treeData.value = [];
+        let result;
+        try {
+          result = await api(props.params);
+        } catch (e) {
+          console.error(e);
+        }
+        if (afterFetch && isFunction(afterFetch)) {
+          result = afterFetch(result);
+        }
+        loading.value = false;
+        if (!result) return;
+        if (!isArray(result)) {
+          result = get(result, props.resultField);
+        }
+        treeData.value = (result as Recordable[]) || [];
+        isFirstLoaded.value = true;
+        emit('options-change', treeData.value);
+      }
+      return { getAttrs, loading, handleChange };
+    },
+  });
+</script>

+ 19 - 7
src/components/Form/src/components/FormItem.vue

@@ -1,17 +1,16 @@
 <script lang="tsx">
   import type { PropType, Ref } from 'vue';
-  import type { FormActionType, FormProps } from '../types/form';
-  import type { FormSchema } from '../types/form';
+  import { computed, defineComponent, toRefs, unref } from 'vue';
+  import type { FormActionType, FormProps, FormSchema } from '../types/form';
   import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
   import type { TableActionType } from '/@/components/Table';
-  import { defineComponent, computed, unref, toRefs } from 'vue';
-  import { Form, Col, Divider } from 'ant-design-vue';
+  import { Col, Divider, Form } from 'ant-design-vue';
   import { componentMap } from '../componentMap';
   import { BasicHelp } from '/@/components/Basic';
   import { isBoolean, isFunction, isNull } from '/@/utils/is';
   import { getSlot } from '/@/utils/helper/tsxHelper';
   import { createPlaceholderMessage, setComponentRuleType } from '../helper';
-  import { upperFirst, cloneDeep } from 'lodash-es';
+  import { cloneDeep, upperFirst } from 'lodash-es';
   import { useItemLabelWidth } from '../hooks/useLabelWidth';
   import { useI18n } from '/@/hooks/web/useI18n';
 
@@ -178,8 +177,21 @@
 
         const getRequired = isFunction(required) ? required(unref(getValues)) : required;
 
-        if ((!rules || rules.length === 0) && getRequired) {
-          rules = [{ required: getRequired, validator }];
+        /*
+         * 1、若设置了required属性,又没有其他的rules,就创建一个验证规则;
+         * 2、若设置了required属性,又存在其他的rules,则只rules中不存在required属性时,才添加验证required的规则
+         *     也就是说rules中的required,优先级大于required
+         */
+        if (getRequired) {
+          if (!rules || rules.length === 0) {
+            rules = [{ required: getRequired, validator }];
+          } else {
+            const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
+
+            if (requiredIndex === -1) {
+              rules.push({ required: getRequired, validator });
+            }
+          }
         }
 
         const requiredRuleIndex: number = rules.findIndex(

+ 6 - 1
src/components/Form/src/hooks/useAdvanced.ts

@@ -1,6 +1,6 @@
 import type { ColEx } from '../types';
 import type { AdvanceState } from '../types/hooks';
-import type { ComputedRef, Ref } from 'vue';
+import { ComputedRef, getCurrentInstance, Ref } from 'vue';
 import type { FormProps, FormSchema } from '../types/form';
 import { computed, unref, watch } from 'vue';
 import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is';
@@ -26,6 +26,8 @@ export default function ({
   formModel,
   defaultValueRef,
 }: UseAdvancedContext) {
+  const vm = getCurrentInstance();
+
   const { realWidthRef, screenEnum, screenRef } = useBreakpoint();
 
   const getEmptySpan = computed((): number => {
@@ -150,6 +152,9 @@ export default function ({
       }
     }
 
+    // 确保页面发送更新
+    vm?.proxy?.$forceUpdate();
+
     advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan);
 
     getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true);

+ 52 - 2
src/components/Form/src/hooks/useFormEvents.ts

@@ -2,7 +2,7 @@ import type { ComputedRef, Ref } from 'vue';
 import type { FormProps, FormSchema, FormActionType } from '../types/form';
 import type { NamePath } from 'ant-design-vue/lib/form/interface';
 import { unref, toRaw, nextTick } from 'vue';
-import { isArray, isFunction, isObject, isString } from '/@/utils/is';
+import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is';
 import { deepMerge } from '/@/utils';
 import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
 import { dateUtil } from '/@/utils/dateUtil';
@@ -39,7 +39,8 @@ export function useFormEvents({
     Object.keys(formModel).forEach((key) => {
       const schema = unref(getSchema).find((item) => item.field === key);
       const isInput = schema?.component && defaultValueComponents.includes(schema.component);
-      formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
+      const defaultValue = cloneDeep(defaultValueRef.value[key]);
+      formModel[key] = isInput ? defaultValue || '' : defaultValue;
     });
     nextTick(() => clearValidate());
 
@@ -55,6 +56,10 @@ export function useFormEvents({
       .map((item) => item.field)
       .filter(Boolean);
 
+    // key 支持 a.b.c 的嵌套写法
+    const delimiter = '.';
+    const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0);
+
     const validKeys: string[] = [];
     Object.keys(values).forEach((key) => {
       const schema = unref(getSchema).find((item) => item.field === key);
@@ -85,6 +90,21 @@ export function useFormEvents({
           formModel[key] = value;
         }
         validKeys.push(key);
+      } else {
+        nestKeyArray.forEach((nestKey: string) => {
+          try {
+            const value = eval('values' + delimiter + nestKey);
+            if (isDef(value)) {
+              formModel[nestKey] = value;
+              validKeys.push(nestKey);
+            }
+          } catch (e) {
+            // key not exist
+            if (isDef(defaultValueRef.value[nestKey])) {
+              formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]);
+            }
+          }
+        });
       }
     });
     validateFields(validKeys).catch((_) => {});
@@ -132,11 +152,14 @@ export function useFormEvents({
     if (!prefixField || index === -1 || first) {
       first ? schemaList.unshift(schema) : schemaList.push(schema);
       schemaRef.value = schemaList;
+      _setDefaultValue(schema);
       return;
     }
     if (index !== -1) {
       schemaList.splice(index + 1, 0, schema);
     }
+    _setDefaultValue(schema);
+
     schemaRef.value = schemaList;
   }
 
@@ -192,9 +215,36 @@ export function useFormEvents({
         }
       });
     });
+    _setDefaultValue(schema);
+
     schemaRef.value = uniqBy(schema, 'field');
   }
 
+  function _setDefaultValue(data: FormSchema | FormSchema[]) {
+    let schemas: FormSchema[] = [];
+    if (isObject(data)) {
+      schemas.push(data as FormSchema);
+    }
+    if (isArray(data)) {
+      schemas = [...data];
+    }
+
+    const obj: Recordable = {};
+    const currentFieldsValue = getFieldsValue();
+    schemas.forEach((item) => {
+      if (
+        item.component != 'Divider' &&
+        Reflect.has(item, 'field') &&
+        item.field &&
+        !isNullOrUnDef(item.defaultValue) &&
+        !(item.field in currentFieldsValue)
+      ) {
+        obj[item.field] = item.defaultValue;
+      }
+    });
+    setFieldsValue(obj);
+  }
+
   function getFieldsValue(): Recordable {
     const formEl = unref(formElRef);
     if (!formEl) return {};

+ 47 - 4
src/components/Form/src/hooks/useFormValues.ts

@@ -3,7 +3,7 @@ import { dateUtil } from '/@/utils/dateUtil';
 import { unref } from 'vue';
 import type { Ref, ComputedRef } from 'vue';
 import type { FormProps, FormSchema } from '../types/form';
-import { set } from 'lodash-es';
+import { cloneDeep, set } from 'lodash-es';
 
 interface UseFormValuesContext {
   defaultValueRef: Ref<any>;
@@ -11,6 +11,43 @@ interface UseFormValuesContext {
   getProps: ComputedRef<FormProps>;
   formModel: Recordable;
 }
+
+/**
+ * @desription deconstruct array-link key. This method will mutate the target.
+ */
+function tryDeconstructArray(key: string, value: any, target: Recordable) {
+  const pattern = /^\[(.+)\]$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = Array.isArray(value) ? value : [value];
+      keys.forEach((k, index) => {
+        set(target, k.trim(), value[index]);
+      });
+      return true;
+    }
+  }
+}
+
+/**
+ * @desription deconstruct object-link key. This method will mutate the target.
+ */
+function tryDeconstructObject(key: string, value: any, target: Recordable) {
+  const pattern = /^\{(.+)\}$/;
+  if (pattern.test(key)) {
+    const match = key.match(pattern);
+    if (match && match[1]) {
+      const keys = match[1].split(',');
+      value = isObject(value) ? value : {};
+      keys.forEach((k) => {
+        set(target, k.trim(), value[k.trim()]);
+      });
+      return true;
+    }
+  }
+}
+
 export function useFormValues({
   defaultValueRef,
   getSchema,
@@ -41,7 +78,10 @@ export function useFormValues({
       if (isString(value)) {
         value = value.trim();
       }
-      set(res, key, value);
+      if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
+        // 没有解构成功的,按原样赋值
+        set(res, key, value);
+      }
     }
     return handleRangeTimeValue(res);
   }
@@ -78,10 +118,13 @@ export function useFormValues({
       const { defaultValue } = item;
       if (!isNullOrUnDef(defaultValue)) {
         obj[item.field] = defaultValue;
-        formModel[item.field] = defaultValue;
+
+        if (formModel[item.field] === undefined) {
+          formModel[item.field] = defaultValue;
+        }
       }
     });
-    defaultValueRef.value = obj;
+    defaultValueRef.value = cloneDeep(obj);
   }
 
   return { handleFormValues, initDefault };

+ 2 - 1
src/components/Form/src/types/form.ts

@@ -10,7 +10,7 @@ import type { RowProps } from 'ant-design-vue/lib/grid/Row';
 export type FieldMapToTime = [string, [string, string], string?][];
 
 export type Rule = RuleObject & {
-  trigger?: 'blur' | 'change' | 'blur' | ['change', 'blur', 'blur'];
+  trigger?: 'blur' | 'change' | ['change', 'blur'];
 };
 
 export interface RenderCallbackParams {
@@ -49,6 +49,7 @@ export type RegisterFn = (formInstance: FormActionType) => void;
 export type UseFormReturnType = [RegisterFn, FormActionType];
 
 export interface FormProps {
+  name?: string;
   layout?: 'vertical' | 'inline' | 'horizontal';
   // Form value
   model?: Recordable;

+ 3 - 2
src/components/Form/src/types/index.ts

@@ -91,6 +91,7 @@ export type ComponentType =
   | 'Select'
   | 'ApiSelect'
   | 'TreeSelect'
+  | 'ApiTree'
   | 'ApiTreeSelect'
   | 'ApiRadioGroup'
   | 'RadioButtonGroup'
@@ -112,5 +113,5 @@ export type ComponentType =
   | 'Render'
   | 'Slider'
   | 'Rate'
-  | 'Time'
-  | 'Divider';
+  | 'Divider'
+  | 'ApiTransfer';

+ 11 - 5
src/components/Table/src/BasicTable.vue

@@ -25,10 +25,16 @@
       <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
         <slot :name="item" v-bind="data || {}"></slot>
       </template>
-
-      <template #[`header-${column.dataIndex}`] v-for="(column, index) in columns" :key="index">
+      <template #headerCell="{ column }">
         <HeaderCell :column="column" />
       </template>
+      <!-- 增加对antdv3.x兼容 -->
+      <template #bodyCell="data">
+        <slot name="bodyCell" v-bind="data || {}"></slot>
+      </template>
+      <!--      <template #[`header-${column.dataIndex}`] v-for="(column, index) in columns" :key="index">-->
+      <!--        <HeaderCell :column="column" />-->
+      <!--      </template>-->
     </Table>
   </div>
 </template>
@@ -247,9 +253,9 @@
           footer: unref(getFooterProps),
           ...unref(getExpandOption),
         };
-        if (slots.expandedRowRender) {
-          propsData = omit(propsData, 'scroll');
-        }
+        // if (slots.expandedRowRender) {
+        //   propsData = omit(propsData, 'scroll');
+        // }
 
         propsData = omit(propsData, ['class', 'onChange']);
         return propsData;

+ 2 - 0
src/components/Table/src/componentMap.ts

@@ -7,6 +7,7 @@ import {
   Switch,
   DatePicker,
   TimePicker,
+  AutoComplete,
 } from 'ant-design-vue';
 import type { ComponentType } from './types/componentType';
 import { ApiSelect, ApiTreeSelect } from '/@/components/Form';
@@ -17,6 +18,7 @@ componentMap.set('Input', Input);
 componentMap.set('InputNumber', InputNumber);
 componentMap.set('Select', Select);
 componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('AutoComplete', AutoComplete);
 componentMap.set('ApiTreeSelect', ApiTreeSelect);
 componentMap.set('Switch', Switch);
 componentMap.set('Checkbox', Checkbox);

+ 1 - 1
src/components/Table/src/components/HeaderCell.vue

@@ -29,7 +29,7 @@
       const { prefixCls } = useDesign('basic-table-header-cell');
 
       const getIsEdit = computed(() => !!props.column?.edit);
-      const getTitle = computed(() => props.column?.customTitle);
+      const getTitle = computed(() => props.column?.customTitle || props.column?.title);
       const getHelpMessage = computed(() => props.column?.helpMessage);
 
       return { prefixCls, getIsEdit, getTitle, getHelpMessage };

+ 1 - 1
src/components/Table/src/components/TableImg.vue

@@ -57,7 +57,7 @@
       const getWrapStyle = computed((): CSSProperties => {
         const { size } = props;
         const s = `${size}px`;
-        return { height: s, width: s };
+        return { height: s, width: s, overflow: 'hidden' };
       });
 
       const { prefixCls } = useDesign('basic-table-img');

+ 103 - 60
src/components/Table/src/components/editable/EditableCell.vue

@@ -1,40 +1,4 @@
-<template>
-  <div :class="prefixCls">
-    <div
-      v-show="!isEdit"
-      :class="{ [`${prefixCls}__normal`]: true, 'ellipsis-cell': column.ellipsis }"
-      @click="handleEdit"
-    >
-      <div class="cell-content" :title="column.ellipsis ? getValues ?? '' : ''">
-        {{ getValues || getValues === 0 ? getValues : '&nbsp;' }}
-      </div>
-      <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
-    </div>
-
-    <a-spin v-if="isEdit" :spinning="spinning">
-      <div :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
-        <CellComponent
-          v-bind="getComponentProps"
-          :component="getComponent"
-          :style="getWrapperStyle"
-          :popoverVisible="getRuleVisible"
-          :rule="getRule"
-          :ruleMessage="ruleMessage"
-          :class="getWrapperClass"
-          ref="elRef"
-          @change="handleChange"
-          @options-change="handleOptionsChange"
-          @pressEnter="handleEnter"
-        />
-        <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
-          <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmitClick" />
-          <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
-        </div>
-      </div>
-    </a-spin>
-  </div>
-</template>
-<script lang="ts">
+<script lang="tsx">
   import type { CSSProperties, PropType } from 'vue';
   import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
   import type { BasicColumn } from '../../types/table';
@@ -50,13 +14,13 @@
   import { propTypes } from '/@/utils/propTypes';
   import { isArray, isBoolean, isFunction, isNumber, isString } from '/@/utils/is';
   import { createPlaceholderMessage } from './helper';
-  import { omit, pick, set } from 'lodash-es';
+  import { pick, set } from 'lodash-es';
   import { treeToList } from '/@/utils/helper/treeHelper';
   import { Spin } from 'ant-design-vue';
 
   export default defineComponent({
     name: 'EditableCell',
-    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, ASpin: Spin },
+    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, Spin },
     directives: {
       clickOutside,
     },
@@ -100,13 +64,6 @@
       });
 
       const getComponentProps = computed(() => {
-        const compProps = props.column?.editComponentProps ?? {};
-        const component = unref(getComponent);
-        const apiSelectProps: Recordable = {};
-        if (component === 'ApiSelect') {
-          apiSelectProps.cache = true;
-        }
-
         const isCheckValue = unref(getIsCheckComp);
 
         const valueField = isCheckValue ? 'checked' : 'value';
@@ -114,19 +71,49 @@
 
         const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
 
+        let compProps = props.column?.editComponentProps ?? {};
+        const { record, column, index } = props;
+
+        if (isFunction(compProps)) {
+          compProps = compProps({ text: val, record, column, index }) ?? {};
+        }
+        const component = unref(getComponent);
+        const apiSelectProps: Recordable = {};
+        if (component === 'ApiSelect') {
+          apiSelectProps.cache = true;
+        }
+        upEditDynamicDisabled(record, column, value);
         return {
           size: 'small',
           getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
-          getCalendarContainer: () => unref(table?.wrapRef.value) ?? document.body,
           placeholder: createPlaceholderMessage(unref(getComponent)),
           ...apiSelectProps,
-          ...omit(compProps, 'onChange'),
+          ...compProps,
           [valueField]: value,
-        };
+          disabled: unref(getDisable),
+        } as any;
+      });
+      function upEditDynamicDisabled(record, column, value) {
+        if (!record) return false;
+        const { key, dataIndex } = column;
+        if (!key && !dataIndex) return;
+        const dataKey = (dataIndex || key) as string;
+        set(record, dataKey, value);
+      }
+      const getDisable = computed(() => {
+        const { editDynamicDisabled } = props.column;
+        let disabled = false;
+        if (isBoolean(editDynamicDisabled)) {
+          disabled = editDynamicDisabled;
+        }
+        if (isFunction(editDynamicDisabled)) {
+          const { record } = props;
+          disabled = editDynamicDisabled({ record });
+        }
+        return disabled;
       });
-
       const getValues = computed(() => {
-        const { editComponentProps, editValueMap } = props.column;
+        const { editValueMap } = props.column;
 
         const value = unref(currentValueRef);
 
@@ -139,7 +126,8 @@
           return value;
         }
 
-        const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
+        const options: LabelValueOptions =
+          unref(getComponentProps)?.options ?? (unref(optionsRef) || []);
         const option = options.find((item) => `${item.value}` === `${value}`);
 
         return option?.label ?? value;
@@ -190,14 +178,16 @@
         const component = unref(getComponent);
         if (!e) {
           currentValueRef.value = e;
-        } else if (e?.target && Reflect.has(e.target, 'value')) {
-          currentValueRef.value = (e as ChangeEvent).target.value;
         } else if (component === 'Checkbox') {
           currentValueRef.value = (e as ChangeEvent).target.checked;
-        } else if (isString(e) || isBoolean(e) || isNumber(e)) {
+        } else if (component === 'Switch') {
+          currentValueRef.value = e;
+        } else if (e?.target && Reflect.has(e.target, 'value')) {
+          currentValueRef.value = (e as ChangeEvent).target.value;
+        } else if (isString(e) || isBoolean(e) || isNumber(e) || isArray(e)) {
           currentValueRef.value = e;
         }
-        const onChange = props.column?.editComponentProps?.onChange;
+        const onChange = unref(getComponentProps)?.onChange;
         if (onChange && isFunction(onChange)) onChange(...arguments);
 
         table.emit?.('edit-change', {
@@ -265,7 +255,7 @@
               result = await beforeEditSubmit({
                 record: pick(record, keys),
                 index,
-                key: key as string,
+                key: dataKey as string,
                 value,
               });
             } catch (e) {
@@ -281,7 +271,7 @@
 
         set(record, dataKey, value);
         //const record = await table.updateTableData(index, dataKey, value);
-        needEmit && table.emit?.('edit-end', { record, index, key, value });
+        needEmit && table.emit?.('edit-end', { record, index, key: dataKey, value });
         isEdit.value = false;
       }
 
@@ -322,7 +312,7 @@
 
       // only ApiSelect or TreeSelect
       function handleOptionsChange(options: LabelValueOptions) {
-        const { replaceFields } = props.column?.editComponentProps ?? {};
+        const { replaceFields } = unref(getComponentProps);
         const component = unref(getComponent);
         if (component === 'ApiTreeSelect') {
           const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
@@ -355,7 +345,7 @@
 
         if (props.column.dataIndex) {
           if (!props.record.editValueRefs) props.record.editValueRefs = {};
-          props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
+          props.record.editValueRefs[props.column.dataIndex as any] = currentValueRef;
         }
         /* eslint-disable  */
         props.record.onCancelEdit = () => {
@@ -398,6 +388,59 @@
         spinning,
       };
     },
+    render() {
+      return (
+        <div class={this.prefixCls}>
+          <div
+            v-show={!this.isEdit}
+            class={{ [`${this.prefixCls}__normal`]: true, 'ellipsis-cell': this.column.ellipsis }}
+            onClick={this.handleEdit}
+          >
+            <div class="cell-content" title={this.column.ellipsis ? this.getValues ?? '' : ''}>
+              {this.column.editRender
+                ? this.column.editRender({
+                    text: this.value,
+                    record: this.record as Recordable,
+                    column: this.column,
+                    index: this.index,
+                  })
+                : this.getValues
+                ? this.getValues
+                : '\u00A0'}
+            </div>
+            {!this.column.editRow && <FormOutlined class={`${this.prefixCls}__normal-icon`} />}
+          </div>
+          {this.isEdit && (
+            <Spin spinning={this.spinning}>
+              <div class={`${this.prefixCls}__wrapper`} v-click-outside={this.onClickOutside}>
+                <CellComponent
+                  {...this.getComponentProps}
+                  component={this.getComponent}
+                  style={this.getWrapperStyle}
+                  popoverVisible={this.getRuleVisible}
+                  rule={this.getRule}
+                  ruleMessage={this.ruleMessage}
+                  class={this.getWrapperClass}
+                  ref="elRef"
+                  onChange={this.handleChange}
+                  onOptionsChange={this.handleOptionsChange}
+                  onPressEnter={this.handleEnter}
+                />
+                {!this.getRowEditable && (
+                  <div class={`${this.prefixCls}__action`}>
+                    <CheckOutlined
+                      class={[`${this.prefixCls}__icon`, 'mx-2']}
+                      onClick={this.handleSubmitClick}
+                    />
+                    <CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
+                  </div>
+                )}
+              </div>
+            </Spin>
+          )}
+        </div>
+      );
+    },
   });
 </script>
 <style lang="less">

+ 1 - 1
src/components/Table/src/components/editable/helper.ts

@@ -7,7 +7,7 @@ const { t } = useI18n();
  * @description: 生成placeholder
  */
 export function createPlaceholderMessage(component: ComponentType) {
-  if (component.includes('Input')) {
+  if (component.includes('Input') || component.includes('AutoComplete')) {
     return t('common.inputText');
   }
   if (component.includes('Picker')) {

+ 17 - 9
src/components/Table/src/components/settings/ColumnSetting.vue

@@ -6,7 +6,7 @@
     <Popover
       placement="bottomLeft"
       trigger="click"
-      @visibleChange="handleVisibleChange"
+      @visible-change="handleVisibleChange"
       :overlayClassName="`${prefixCls}__cloumn-list`"
       :getPopupContainer="getPopupContainer"
     >
@@ -111,6 +111,7 @@
     computed,
   } from 'vue';
   import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue';
+  import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface';
   import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue';
   import { Icon } from '/@/components/Icon';
   import { ScrollContainer } from '/@/components/Container';
@@ -182,10 +183,12 @@
       });
 
       watchEffect(() => {
-        const columns = table.getColumns();
-        if (columns.length && !state.isInit) {
-          init();
-        }
+        setTimeout(() => {
+          const columns = table.getColumns();
+          if (columns.length && !state.isInit) {
+            init();
+          }
+        }, 0);
       });
 
       watchEffect(() => {
@@ -241,7 +244,7 @@
       }
 
       // checkAll change
-      function onCheckAllChange(e: ChangeEvent) {
+      function onCheckAllChange(e: CheckboxChangeEvent) {
         const checkList = plainOptions.value.map((item) => item.value);
         if (e.target.checked) {
           state.checkedList = checkList;
@@ -313,7 +316,12 @@
               }
 
               plainSortOptions.value = columns;
-              setColumns(columns);
+
+              setColumns(
+                columns
+                  .map((col: Options) => col.value)
+                  .filter((value: string) => state.checkedList.includes(value)),
+              );
             },
           });
           // 记录原始order 序列
@@ -323,14 +331,14 @@
       }
 
       // Control whether the serial number column is displayed
-      function handleIndexCheckChange(e: ChangeEvent) {
+      function handleIndexCheckChange(e: CheckboxChangeEvent) {
         table.setProps({
           showIndexColumn: e.target.checked,
         });
       }
 
       // Control whether the check box is displayed
-      function handleSelectCheckChange(e: ChangeEvent) {
+      function handleSelectCheckChange(e: CheckboxChangeEvent) {
         table.setProps({
           rowSelection: e.target.checked ? defaultRowSelection : undefined,
         });

+ 1 - 1
src/components/Table/src/components/settings/SizeSetting.vue

@@ -4,7 +4,7 @@
       <span>{{ t('component.table.settingDens') }}</span>
     </template>
 
-    <Dropdown placement="bottomCenter" :trigger="['click']" :getPopupContainer="getPopupContainer">
+    <Dropdown placement="bottom" :trigger="['click']" :getPopupContainer="getPopupContainer">
       <ColumnHeightOutlined />
       <template #overlay>
         <Menu @click="handleTitleClick" selectable v-model:selectedKeys="selectedKeysRef">

+ 1 - 1
src/components/Table/src/components/settings/index.vue

@@ -44,7 +44,7 @@
         return {
           redo: true,
           size: true,
-          setting: false,
+          setting: true,
           fullScreen: false,
           ...props.setting,
         };

+ 11 - 11
src/components/Table/src/hooks/useColumns.ts

@@ -1,7 +1,7 @@
 import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table';
 import type { PaginationProps } from '../types/pagination';
 import type { ComputedRef } from 'vue';
-import { computed, Ref, ref, toRaw, unref, watch } from 'vue';
+import { computed, Ref, ref, reactive, toRaw, unref, watch } from 'vue';
 import { renderEditCell } from '../components/editable';
 import { usePermission } from '/@/hooks/web/usePermission';
 import { useI18n } from '/@/hooks/web/useI18n';
@@ -152,10 +152,10 @@ export function useColumns(
         return hasPermission(column.auth) && isIfShow(column);
       })
       .map((column) => {
-        const { slots, dataIndex, customRender, format, edit, editRow, flag } = column;
+        const { slots, customRender, format, edit, editRow, flag } = column;
 
         if (!slots || !slots?.title) {
-          column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
+          // column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
           column.customTitle = column.title;
           Reflect.deleteProperty(column, 'title');
         }
@@ -170,7 +170,7 @@ export function useColumns(
         if ((edit || editRow) && !isDefaultAction) {
           column.customRender = renderEditCell(column);
         }
-        return column;
+        return reactive(column);
       });
   });
 
@@ -197,7 +197,7 @@ export function useColumns(
    * set columns
    * @param columnList key|column
    */
-  function setColumns(columnList: Partial<BasicColumn>[] | string[]) {
+  function setColumns(columnList: Partial<BasicColumn>[] | (string | string[])[]) {
     const columns = cloneDeep(columnList);
     if (!isArray(columns)) return;
 
@@ -210,23 +210,23 @@ export function useColumns(
 
     const cacheKeys = cacheColumns.map((item) => item.dataIndex);
 
-    if (!isString(firstColumn)) {
+    if (!isString(firstColumn) && !isArray(firstColumn)) {
       columnsRef.value = columns as BasicColumn[];
     } else {
-      const columnKeys = columns as string[];
+      const columnKeys = (columns as (string | string[])[]).map((m) => m.toString());
       const newColumns: BasicColumn[] = [];
       cacheColumns.forEach((item) => {
         newColumns.push({
           ...item,
-          defaultHidden: !columnKeys.includes(item.dataIndex! || (item.key as string)),
+          defaultHidden: !columnKeys.includes(item.dataIndex?.toString() || (item.key as string)),
         });
       });
       // Sort according to another array
       if (!isEqual(cacheKeys, columns)) {
         newColumns.sort((prev, next) => {
           return (
-            columnKeys.indexOf(prev.dataIndex as string) -
-            columnKeys.indexOf(next.dataIndex as string)
+            columnKeys.indexOf(prev.dataIndex?.toString() as string) -
+            columnKeys.indexOf(next.dataIndex?.toString() as string)
           );
         });
       }
@@ -298,7 +298,7 @@ export function formatCell(text: string, format: CellFormat, record: Recordable,
   try {
     // date type
     const DATE_FORMAT_PREFIX = 'date|';
-    if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) {
+    if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX) && text) {
       const dateFormat = format.replace(DATE_FORMAT_PREFIX, '');
 
       if (!dateFormat) {

+ 1 - 1
src/components/Table/src/hooks/useDataSource.ts

@@ -292,7 +292,7 @@ export function useDataSource(
       const isArrayResult = Array.isArray(res);
 
       let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
-      const resultTotal: number = isArrayResult ? 0 : get(res, totalField);
+      const resultTotal: number = isArrayResult ? res.length : get(res, totalField);
 
       // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行
       if (resultTotal) {

+ 2 - 3
src/components/Table/src/hooks/useTableFooter.ts

@@ -36,14 +36,13 @@ export function useTableFooter(
     nextTick(() => {
       const tableEl = unref(tableElRef);
       if (!tableEl) return;
-      const bodyDomList = tableEl.$el.querySelectorAll('.ant-table-body');
-      const bodyDom = bodyDomList[0];
+      const bodyDom = tableEl.$el.querySelector('.ant-table-content');
       useEventListener({
         el: bodyDom,
         name: 'scroll',
         listener: () => {
           const footerBodyDom = tableEl.$el.querySelector(
-            '.ant-table-footer .ant-table-body',
+            '.ant-table-footer .ant-table-content',
           ) as HTMLDivElement;
           if (!footerBodyDom || !bodyDom) return;
           footerBodyDom.scrollLeft = bodyDom.scrollLeft;

+ 2 - 2
src/components/Table/src/hooks/useTableScroll.ts

@@ -90,7 +90,7 @@ export function useTableScroll(
 
     bodyEl!.style.height = 'unset';
 
-    if (!unref(getCanResize) || tableData.length === 0) return;
+    if (!unref(getCanResize) || !unref(tableData) || tableData.length === 0) return;
 
     await nextTick();
     // Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
@@ -191,7 +191,7 @@ export function useTableScroll(
 
     const columns = unref(columnsRef).filter((item) => !item.defaultHidden);
     columns.forEach((item) => {
-      width += Number.parseInt(item.width as string) || 0;
+      width += Number.parseFloat(item.width as string) || 0;
     });
     const unsetWidthColumns = columns.filter((item) => !Reflect.has(item, 'width'));
 

+ 1 - 0
src/components/Table/src/types/componentType.ts

@@ -3,6 +3,7 @@ export type ComponentType =
   | 'InputNumber'
   | 'Select'
   | 'ApiSelect'
+  | 'AutoComplete'
   | 'ApiTreeSelect'
   | 'Checkbox'
   | 'Switch'

+ 17 - 1
src/components/Table/src/types/pagination.ts

@@ -7,9 +7,18 @@ interface PaginationRenderProps {
   originalElement: any;
 }
 
+type PaginationPositon =
+  | 'topLeft'
+  | 'topCenter'
+  | 'topRight'
+  | 'bottomLeft'
+  | 'bottomCenter'
+  | 'bottomRight';
+
 export declare class PaginationConfig extends Pagination {
-  position?: 'top' | 'bottom' | 'both';
+  position?: PaginationPositon[];
 }
+
 export interface PaginationProps {
   /**
    * total number of data items
@@ -96,4 +105,11 @@ export interface PaginationProps {
    * @type Function
    */
   itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
+
+  /**
+   * specify the position of Pagination
+   * @default ['bottomRight']
+   * @type string[]
+   */
+  position?: PaginationPositon[];
 }

+ 18 - 2
src/components/Table/src/types/table.ts

@@ -412,7 +412,7 @@ export type CellFormat =
   | Map<string | number, any>;
 
 // @ts-ignore
-export interface BasicColumn extends ColumnProps {
+export interface BasicColumn extends ColumnProps<Recordable> {
   children?: BasicColumn[];
   filters?: {
     text: string;
@@ -441,7 +441,14 @@ export interface BasicColumn extends ColumnProps {
   editRow?: boolean;
   editable?: boolean;
   editComponent?: ComponentType;
-  editComponentProps?: Recordable;
+  editComponentProps?:
+    | ((opt: {
+        text: string | number | boolean | Recordable;
+        record: Recordable;
+        column: BasicColumn;
+        index: number;
+      }) => Recordable)
+    | Recordable;
   editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
   editValueMap?: (value: any) => string;
   onEditRow?: () => void;
@@ -449,6 +456,15 @@ export interface BasicColumn extends ColumnProps {
   auth?: RoleEnum | RoleEnum[] | string | string[];
   // 业务控制是否显示
   ifShow?: boolean | ((column: BasicColumn) => boolean);
+  // 自定义修改后显示的内容
+  editRender?: (opt: {
+    text: string | number | boolean | Recordable;
+    record: Recordable;
+    column: BasicColumn;
+    index: number;
+  }) => VNodeChild | JSX.Element;
+  // 动态 Disabled
+  editDynamicDisabled?: boolean | ((record: Recordable) => boolean);
 }
 
 export type ColumnChangeParam = {

+ 4 - 0
src/layouts/default/header/components/lock/LockModal.vue

@@ -53,6 +53,10 @@
           {
             field: 'password',
             label: t('layout.header.lockScreenPassword'),
+            labelWidth: 80,
+            colProps: {
+              span: 24,
+            },
             component: 'InputPassword',
             required: true,
           },

+ 60 - 20
src/layouts/default/header/components/notify/index.vue

@@ -1,10 +1,10 @@
 <template>
   <div :class="prefixCls">
     <Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
-      <Badge :count="count" dot :numberStyle="numberStyle">
+      <Badge size="small" dot :count="count" :numberStyle="numberStyle" @click="onNoticeClick">
         <BellOutlined />
       </Badge>
-      <template #content>
+      <!-- <template #content>
         <Tabs>
           <template v-for="item in listData" :key="item.key">
             <TabPane>
@@ -12,44 +12,83 @@
                 {{ item.name }}
                 <span v-if="item.list.length !== 0">({{ item.list.length }})</span>
               </template>
-              <!-- 绑定title-click事件的通知列表中标题是“可点击”的-->
               <NoticeList :list="item.list" v-if="item.key === '1'" @title-click="onNoticeClick" />
               <NoticeList :list="item.list" v-else />
             </TabPane>
           </template>
         </Tabs>
-      </template>
+      </template> -->
     </Popover>
   </div>
 </template>
 <script lang="ts">
-  import { computed, defineComponent, ref } from 'vue';
-  import { Popover, Tabs, Badge } from 'ant-design-vue';
+  import { onMounted, defineComponent, ref, h } from 'vue';
+  // Tabs, Badge
+  import { Popover, Badge } from 'ant-design-vue';
   import { BellOutlined } from '@ant-design/icons-vue';
-  import { tabListData, ListItem } from './data';
-  import NoticeList from './NoticeList.vue';
+  import { tabListData } from './data';
+  // import NoticeList from './NoticeList.vue';
   import { useDesign } from '/@/hooks/web/useDesign';
+  // import { useMessage } from '/@/hooks/web/useMessage';
+  import { unreadNoticeApi, readAllApi } from '/@/api/notification/list';
+  import { useGo } from '/@/hooks/web/usePage';
   import { useMessage } from '/@/hooks/web/useMessage';
-
+  import { TableImg } from '/@/components/Table';
+  // Tabs, TabPane: Tabs.TabPane, Badge, NoticeList
   export default defineComponent({
-    components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
+    components: { Popover, BellOutlined, Badge },
     setup() {
       const { prefixCls } = useDesign('header-notify');
-      const { createMessage } = useMessage();
+      // const { createMessage } = useMessage();
+
       const listData = ref(tabListData);
+      const go = useGo();
+      const { createWarningModal } = useMessage();
 
-      const count = computed(() => {
-        let count = 0;
-        for (let i = 0; i < tabListData.length; i++) {
-          count += tabListData[i].list.length;
+      const count = ref(0);
+      async function fetchNotice() {
+        const unread = await unreadNoticeApi();
+        console.log('unread', unread.totalCount);
+        count.value = unread.totalCount;
+        interface Unread {
+          title: string;
+          type: number;
+          content: string;
         }
-        return count;
+        if (unread.list?.length) {
+          const list = unread.list[0] as unknown as Unread;
+          if (list.type === 0) {
+            createWarningModal({
+              title: list.title,
+              content: list.content,
+              onOk: async () => {
+                await readAllApi();
+                await fetchNotice();
+              },
+            });
+          }
+          if (list.type === 1) {
+            createWarningModal({
+              title: list.title,
+              content: h(TableImg, {
+                size: 330,
+                imgList: [list.content],
+              }),
+              onOk: async () => {
+                await readAllApi();
+                await fetchNotice();
+              },
+            });
+          }
+        }
+      }
+      onMounted(async () => {
+        fetchNotice();
       });
 
-      function onNoticeClick(record: ListItem) {
-        createMessage.success('你点击了通知,ID=' + record.id);
-        // 可以直接将其标记为已读(为标题添加删除线),此处演示的代码会切换删除线状态
-        record.titleDelete = !record.titleDelete;
+      function onNoticeClick() {
+        go('/notification');
+        count.value = 0;
       }
 
       return {
@@ -81,6 +120,7 @@
 
       .ant-badge-multiple-words {
         padding: 0 4px;
+        transform: translate(50%, 50%);
       }
 
       svg {

+ 1 - 0
src/layouts/default/header/components/user-dropdown/DropMenuItem.vue

@@ -18,6 +18,7 @@
     name: 'DropdownMenuItem',
     components: { MenuItem: Menu.Item, Icon },
     props: {
+      // eslint-disable-next-line
       key: propTypes.string,
       text: propTypes.string,
       icon: propTypes.string,

+ 2 - 2
src/layouts/default/header/index.vue

@@ -78,7 +78,7 @@
   import { SettingButtonPositionEnum } from '/@/enums/appEnum';
   import { AppLocalePicker } from '/@/components/Application';
   // Notify ErrorAction
-  import { UserDropDown, LayoutBreadcrumb, FullScreen } from './components';
+  import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify } from './components';
   import { useAppInject } from '/@/hooks/web/useAppInject';
   import { useDesign } from '/@/hooks/web/useDesign';
 
@@ -98,7 +98,7 @@
       AppLocalePicker,
       FullScreen,
       Tag,
-      // Notify,
+      Notify,
       // AppSearch,
       // ErrorAction,
       SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {

+ 1 - 1
src/layouts/default/sider/MixSider.vue

@@ -66,7 +66,7 @@
           :items="childrenMenus"
           :theme="getMenuTheme"
           mixSider
-          @menuClick="handleMenuClick"
+          @menu-click="handleMenuClick"
         />
       </ScrollContainer>
       <div

+ 9 - 5
src/layouts/default/sider/useLayoutSider.ts

@@ -1,26 +1,30 @@
 import type { Ref } from 'vue';
 
-import { computed, unref, onMounted, nextTick, ref } from 'vue';
+import { computed, unref, onMounted, nextTick } from 'vue';
 
 import { TriggerEnum } from '/@/enums/menuEnum';
 
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 import { useDebounceFn } from '@vueuse/core';
+import { useAppStore } from '/@/store/modules/app';
 
 /**
  * Handle related operations of menu events
  */
 export function useSiderEvent() {
-  const brokenRef = ref(false);
-
+  const appStore = useAppStore();
   const { getMiniWidthNumber } = useMenuSetting();
 
   const getCollapsedWidth = computed(() => {
-    return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
+    return unref(getMiniWidthNumber);
   });
 
   function onBreakpointChange(broken: boolean) {
-    brokenRef.value = broken;
+    appStore.setProjectConfig({
+      menuSetting: {
+        siderHidden: broken,
+      },
+    });
   }
 
   return { getCollapsedWidth, onBreakpointChange };

+ 7 - 1
src/layouts/default/tabs/components/TabContent.vue

@@ -1,5 +1,11 @@
 <template>
-  <Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent">
+  <Dropdown
+    :dropMenuList="getDropMenuList"
+    :trigger="getTrigger"
+    placement="bottom"
+    overlayClassName="multiple-tabs__dropdown"
+    @menu-event="handleMenuEvent"
+  >
     <div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="getIsTabs">
       <span class="ml-1">{{ getTitle }}</span>
     </div>

+ 25 - 0
src/layouts/default/tabs/index.less

@@ -180,3 +180,28 @@ html[data-theme='light'] {
     }
   }
 }
+
+.ant-tabs-dropdown-menu {
+  &-title-content {
+    display: flex;
+    align-items: center;
+
+    .@{prefix-cls} {
+      &-content__info {
+        width: auto;
+        margin-left: 0;
+        line-height: 28px;
+      }
+    }
+  }
+
+  &-item-remove {
+    margin-left: auto;
+  }
+}
+
+.multiple-tabs__dropdown {
+  .ant-dropdown-content {
+    width: 172px;
+  }
+}

+ 3 - 1
src/layouts/page/index.vue

@@ -17,7 +17,9 @@
         <keep-alive v-if="openCache" :include="getCaches">
           <component :is="Component" :key="route.fullPath" />
         </keep-alive>
-        <component v-else :is="Component" :key="route.fullPath" />
+        <div v-else :key="route.name">
+          <component :is="Component" :key="route.fullPath" />
+        </div>
       </transition>
     </template>
   </RouterView>

+ 19 - 0
src/locales/lang/zh-CN/antdLocale/DatePicker.ts

@@ -0,0 +1,19 @@
+export default {
+  lang: {
+    shortWeekDays: ['一', '二', '三', '四', '五', '六', '日'],
+    shortMonths: [
+      '1月',
+      '2月',
+      '3月',
+      '4月',
+      '5月',
+      '6月',
+      '7月',
+      '8月',
+      '9月',
+      '10月',
+      '11月',
+      '12月',
+    ],
+  },
+};

+ 105 - 103
src/locales/lang/zh-CN/sys.ts

@@ -1,103 +1,105 @@
-export default {
-  api: {
-    operationFailed: '操作失败',
-    errorTip: '错误提示',
-    errorMessage: '操作失败,系统异常!',
-    timeoutMessage: '登录超时,请重新登录!',
-    apiTimeoutMessage: '接口请求超时,请刷新页面重试!',
-    apiRequestFailed: '请求出错,请稍候重试',
-    networkException: '网络异常',
-    networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!',
-
-    errMsg401: '用户没有权限(令牌、用户名、密码错误)!',
-    errMsg403: '用户得到授权,但是访问是被禁止的。!',
-    errMsg404: '网络请求错误,未找到该资源!',
-    errMsg405: '网络请求错误,请求方法未允许!',
-    errMsg408: '网络请求超时!',
-    errMsg500: '服务器错误,请联系管理员!',
-    errMsg501: '网络未实现!',
-    errMsg502: '网络错误!',
-    errMsg503: '服务不可用,服务器暂时过载或维护!',
-    errMsg504: '网络超时!',
-    errMsg505: 'http版本不支持该请求!',
-  },
-  app: { logoutTip: '温馨提醒', logoutMessage: '是否确认退出系统?', menuLoading: '菜单加载中...' },
-  errorLog: {
-    tableTitle: '错误日志列表',
-    tableColumnType: '类型',
-    tableColumnDate: '时间',
-    tableColumnFile: '文件',
-    tableColumnMsg: '错误信息',
-    tableColumnStackMsg: 'stack信息',
-
-    tableActionDesc: '详情',
-
-    modalTitle: '错误详情',
-
-    fireVueError: '点击触发vue错误',
-    fireResourceError: '点击触发资源加载错误',
-    fireAjaxError: '点击触发ajax错误',
-
-    enableMessage: '只在`/src/settings/projectSetting.ts` 内的useErrorHandle=true时生效.',
-  },
-  exception: {
-    backLogin: '返回登录',
-    backHome: '返回首页',
-    subTitle403: '抱歉,您无权访问此页面。',
-    subTitle404: '抱歉,您访问的页面不存在。',
-    subTitle500: '抱歉,服务器报告错误。',
-    noDataTitle: '当前页无数据',
-    networkErrorTitle: '网络错误',
-    networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!',
-  },
-  lock: {
-    unlock: '点击解锁',
-    alert: '锁屏密码错误',
-    backToLogin: '返回登录',
-    entry: '进入系统',
-    placeholder: '请输入锁屏密码或者用户密码',
-  },
-  login: {
-    backSignIn: '返回',
-    signInFormTitle: '登录',
-    mobileSignInFormTitle: '手机登录',
-    qrSignInFormTitle: '二维码登录',
-    signUpFormTitle: '注册',
-    forgetFormTitle: '重置密码',
-
-    signInTitle: '10分钟搞定实景VR',
-    signInDesc: '自助720度实景VR发布管理,自助、简单、安全、高效',
-    policy: '我同意xxx隐私政策',
-    scanSign: `扫码后点击"确认",即可完成登录`,
-
-    loginButton: '登录',
-    registerButton: '注册',
-    rememberMe: '记住我',
-    forgetPassword: '忘记密码?',
-    otherSignIn: '其他登录方式',
-
-    // notify
-    loginSuccessTitle: '登录成功',
-    loginSuccessDesc: '欢迎回来',
-
-    // placeholder
-    accountPlaceholder: '请输入账号',
-    passwordPlaceholder: '请输入密码',
-    passwordFormat: '输入密码格式有误',
-    smsPlaceholder: '请输入验证码',
-    mobilePlaceholder: '请输入手机号码',
-    policyPlaceholder: '勾选后才能注册',
-    diffPwd: '两次输入密码不一致',
-
-    userName: '账号',
-    password: '密码',
-    confirmPassword: '确认密码',
-    email: '邮箱',
-    smsCode: '短信验证码',
-    mobile: '手机号码',
-
-    loginDragValidate: '登录需要拖拽验证',
-    loginAgain: '请再次操作',
-    captcha: '验证码',
-  },
-};
+export default {
+  api: {
+    operationFailed: '操作失败',
+    errorTip: '错误提示',
+    errorMessage: '操作失败,系统异常!',
+    timeoutMessage: '登录超时,请重新登录!',
+    apiTimeoutMessage: '接口请求超时,请刷新页面重试!',
+    apiRequestFailed: '请求出错,请稍候重试',
+    networkException: '网络异常',
+    networkExceptionMsg: '网络异常,请检查您的网络连接是否正常!',
+
+    errMsg401: '用户没有权限(令牌、用户名、密码错误)!',
+    errMsg403: '用户得到授权,但是访问是被禁止的。!',
+    errMsg404: '网络请求错误,未找到该资源!',
+    errMsg405: '网络请求错误,请求方法未允许!',
+    errMsg408: '网络请求超时!',
+    errMsg500: '服务器错误,请联系管理员!',
+    errMsg501: '网络未实现!',
+    errMsg502: '网络错误!',
+    errMsg503: '服务不可用,服务器暂时过载或维护!',
+    errMsg504: '网络超时!',
+    errMsg505: 'http版本不支持该请求!',
+  },
+  app: { logoutTip: '温馨提醒', logoutMessage: '是否确认退出系统?', menuLoading: '菜单加载中...' },
+  errorLog: {
+    tableTitle: '错误日志列表',
+    tableColumnType: '类型',
+    tableColumnDate: '时间',
+    tableColumnFile: '文件',
+    tableColumnMsg: '错误信息',
+    tableColumnStackMsg: 'stack信息',
+
+    tableActionDesc: '详情',
+
+    modalTitle: '错误详情',
+
+    fireVueError: '点击触发vue错误',
+    fireResourceError: '点击触发资源加载错误',
+    fireAjaxError: '点击触发ajax错误',
+
+    enableMessage: '只在`/src/settings/projectSetting.ts` 内的useErrorHandle=true时生效.',
+  },
+  exception: {
+    backLogin: '返回登录',
+    backHome: '返回首页',
+    subTitle403: '抱歉,您无权访问此页面。',
+    subTitle404: '抱歉,您访问的页面不存在。',
+    subTitle500: '抱歉,服务器报告错误。',
+    noDataTitle: '当前页无数据',
+    networkErrorTitle: '网络错误',
+    networkErrorSubTitle: '抱歉,您的网络连接已断开,请检查您的网络!',
+  },
+  lock: {
+    unlock: '点击解锁',
+    alert: '锁屏密码错误',
+    backToLogin: '返回登录',
+    entry: '进入系统',
+    placeholder: '请输入锁屏密码或者用户密码',
+  },
+  login: {
+    backSignIn: '返回',
+    signInFormTitle: '登录',
+    mobileSignInFormTitle: '手机登录',
+    qrSignInFormTitle: '二维码登录',
+    signUpFormTitle: '注册',
+    forgetFormTitle: '重置密码',
+
+    signInTitle: '10分钟搞定实景VR',
+    signInDesc: '自助720度实景VR发布管理,自助、简单、安全、高效',
+    policy: '我同意xxx隐私政策',
+    scanSign: `扫码后点击"确认",即可完成登录`,
+
+    loginButton: '登录',
+    registerButton: '注册',
+    rememberMe: '记住我',
+    forgetPassword: '忘记密码?',
+    otherSignIn: '其他登录方式',
+
+    // notify
+    loginSuccessTitle: '登录成功',
+    loginSuccessDesc: '欢迎回来',
+
+    // placeholder
+    accountPlaceholder: '请输入账号',
+    passwordPlaceholder: '请输入密码',
+    passwordFormat: '输入密码格式有误',
+    smsPlaceholder: '请输入验证码',
+    mobilePlaceholder: '请输入手机号码',
+    policyPlaceholder: '勾选后才能注册',
+    diffPwd: '两次输入密码不一致',
+
+    mobileFormat: '输入手机格式有误',
+
+    userName: '账号',
+    password: '密码',
+    confirmPassword: '确认密码',
+    email: '邮箱',
+    smsCode: '短信验证码',
+    mobile: '手机号码',
+
+    loginDragValidate: '登录需要拖拽验证',
+    loginAgain: '请再次操作',
+    captcha: '验证码',
+  },
+};

+ 11 - 10
src/locales/lang/zh_CN.ts

@@ -1,10 +1,11 @@
-import { genMessage } from '../helper';
-import antdLocale from 'ant-design-vue/es/locale/zh_CN';
-
-const modules = import.meta.globEager('./zh-CN/**/*.ts');
-export default {
-  message: {
-    ...genMessage(modules, 'zh-CN'),
-    antdLocale,
-  },
-};
+import { genMessage } from '../helper';
+import antdLocale from 'ant-design-vue/es/locale/zh_CN';
+
+const modules = import.meta.globEager('./zh-CN/**/*.ts');
+export default {
+  message: {
+    ...genMessage(modules, 'zh-CN'),
+    antdLocale,
+  },
+  // momentLocaleName: 'zh-cn',
+};

+ 69 - 69
src/locales/useLocale.ts

@@ -1,69 +1,69 @@
-/**
- * Multi-language related operations
- */
-import type { LocaleType } from '/#/config';
-
-import { i18n } from './setupI18n';
-import { useLocaleStoreWithOut } from '/@/store/modules/locale';
-import { unref, computed } from 'vue';
-import { loadLocalePool, setHtmlPageLang } from './helper';
-
-interface LangModule {
-  message: Recordable;
-  dateLocale: Recordable;
-  dateLocaleName: string;
-}
-
-function setI18nLanguage(locale: LocaleType) {
-  const localeStore = useLocaleStoreWithOut();
-
-  if (i18n.mode === 'legacy') {
-    i18n.global.locale = locale;
-  } else {
-    (i18n.global.locale as any).value = locale;
-  }
-  localeStore.setLocaleInfo({ locale });
-  setHtmlPageLang(locale);
-}
-
-export function useLocale() {
-  const localeStore = useLocaleStoreWithOut();
-  const getLocale = computed(() => localeStore.getLocale);
-  const getShowLocalePicker = computed(() => localeStore.getShowPicker);
-
-  const getAntdLocale = computed((): any => {
-    return i18n.global.getLocaleMessage(unref(getLocale))?.antdLocale ?? {};
-  });
-
-  // Switching the language will change the locale of useI18n
-  // And submit to configuration modification
-  async function changeLocale(locale: LocaleType) {
-    const globalI18n = i18n.global;
-    const currentLocale = unref(globalI18n.locale);
-    if (currentLocale === locale) {
-      return locale;
-    }
-
-    if (loadLocalePool.includes(locale)) {
-      setI18nLanguage(locale);
-      return locale;
-    }
-    const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule;
-    if (!langModule) return;
-
-    const { message } = langModule;
-
-    globalI18n.setLocaleMessage(locale, message);
-    loadLocalePool.push(locale);
-
-    setI18nLanguage(locale);
-    return locale;
-  }
-
-  return {
-    getLocale,
-    getShowLocalePicker,
-    changeLocale,
-    getAntdLocale,
-  };
-}
+/**
+ * Multi-language related operations
+ */
+import type { LocaleType } from '/#/config';
+
+import { i18n } from './setupI18n';
+import { useLocaleStoreWithOut } from '/@/store/modules/locale';
+import { unref, computed } from 'vue';
+import { loadLocalePool, setHtmlPageLang } from './helper';
+
+interface LangModule {
+  message: Recordable;
+  dateLocale: Recordable;
+  dateLocaleName: string;
+}
+
+function setI18nLanguage(locale: LocaleType) {
+  const localeStore = useLocaleStoreWithOut();
+
+  if (i18n.mode === 'legacy') {
+    i18n.global.locale = locale;
+  } else {
+    (i18n.global.locale as any).value = locale;
+  }
+  localeStore.setLocaleInfo({ locale });
+  setHtmlPageLang(locale);
+}
+
+export function useLocale() {
+  const localeStore = useLocaleStoreWithOut();
+  const getLocale = computed(() => localeStore.getLocale);
+  const getShowLocalePicker = computed(() => localeStore.getShowPicker);
+
+  const getAntdLocale = computed((): any => {
+    return i18n.global.getLocaleMessage(unref(getLocale))?.antdLocale ?? {};
+  });
+
+  // Switching the language will change the locale of useI18n
+  // And submit to configuration modification
+  async function changeLocale(locale: LocaleType) {
+    const globalI18n = i18n.global;
+    const currentLocale = unref(globalI18n.locale);
+    if (currentLocale === locale) {
+      return locale;
+    }
+
+    if (loadLocalePool.includes(locale)) {
+      setI18nLanguage(locale);
+      return locale;
+    }
+    const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule;
+    if (!langModule) return;
+
+    const { message } = langModule;
+
+    globalI18n.setLocaleMessage(locale, message);
+    loadLocalePool.push(locale);
+
+    setI18nLanguage(locale);
+    return locale;
+  }
+
+  return {
+    getLocale,
+    getShowLocalePicker,
+    changeLocale,
+    getAntdLocale,
+  };
+}

+ 78 - 69
src/router/routes/index.ts

@@ -1,69 +1,78 @@
-import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
-
-import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
-
-import { mainOutRoutes } from './mainOut';
-import { PageEnum } from '/@/enums/pageEnum';
-import { t } from '/@/hooks/web/useI18n';
-import { LAYOUT } from '/@/router/constant';
-const modules = import.meta.globEager('./modules/**/*.ts');
-
-const routeModuleList: AppRouteModule[] = [];
-
-Object.keys(modules).forEach((key) => {
-  const mod = modules[key].default || {};
-  const modList = Array.isArray(mod) ? [...mod] : [mod];
-  routeModuleList.push(...modList);
-});
-
-export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
-
-export const RootRoute: AppRouteRecordRaw = {
-  path: '/',
-  name: 'Root',
-  redirect: PageEnum.BASE_HOME,
-  meta: {
-    title: 'Root',
-  },
-};
-
-export const LoginRoute: AppRouteRecordRaw = {
-  path: '/login',
-  name: 'Login',
-  component: () => import('/@/views/sys/login/Login.vue'),
-  meta: {
-    title: t('routes.basic.login'),
-  },
-};
-
-export const WelcomeRoute: AppRouteRecordRaw = {
-  path: '/welcome',
-  name: 'Welcome',
-  component: LAYOUT,
-  redirect: '/welcome/index',
-  // component: () => import('/@/views/welcome/index.vue'),
-  meta: {
-    title: t('routes.basic.welcome'),
-  },
-  children: [
-    {
-      path: 'index',
-      name: 'welcome',
-      component: () => import('/@/views/welcome/index.vue'),
-      meta: {
-        title: t('routes.basic.welcomeLogin'),
-        hideMenu: true,
-      },
-    },
-  ],
-};
-
-// Basic routing without permission
-export const basicRoutes = [
-  LoginRoute,
-  WelcomeRoute,
-  RootRoute,
-  ...mainOutRoutes,
-  REDIRECT_ROUTE,
-  PAGE_NOT_FOUND_ROUTE,
-];
+import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
+
+import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
+
+import { mainOutRoutes } from './mainOut';
+import { PageEnum } from '/@/enums/pageEnum';
+import { t } from '/@/hooks/web/useI18n';
+import { LAYOUT } from '/@/router/constant';
+const modules = import.meta.globEager('./modules/**/*.ts');
+
+const routeModuleList: AppRouteModule[] = [];
+
+Object.keys(modules).forEach((key) => {
+  const mod = modules[key].default || {};
+  const modList = Array.isArray(mod) ? [...mod] : [mod];
+  routeModuleList.push(...modList);
+});
+
+export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
+
+export const RootRoute: AppRouteRecordRaw = {
+  path: '/',
+  name: 'Root',
+  redirect: PageEnum.BASE_HOME,
+  meta: {
+    title: 'Root',
+  },
+};
+
+export const LoginRoute: AppRouteRecordRaw = {
+  path: '/login',
+  name: 'Login',
+  component: () => import('/@/views/sys/login/Login.vue'),
+  meta: {
+    title: t('routes.basic.login'),
+  },
+};
+
+export const WelcomeRoute: AppRouteRecordRaw = {
+  path: '/welcome',
+  name: 'Welcome',
+  component: LAYOUT,
+  redirect: '/welcome/index',
+  // component: () => import('/@/views/welcome/index.vue'),
+  meta: {
+    title: t('routes.basic.welcome'),
+  },
+  children: [
+    {
+      path: 'index',
+      name: 'welcome',
+      component: () => import('/@/views/welcome/index.vue'),
+      meta: {
+        title: t('routes.basic.welcomeLogin'),
+        hideMenu: true,
+      },
+    },
+    {
+      path: 'analysis',
+      name: 'analysis',
+      component: () => import('/@/views/dashboard/analysis/index.vue'),
+      meta: {
+        title: t('routes.dashboard.analysis'),
+        hideMenu: true,
+      },
+    },
+  ],
+};
+
+// Basic routing without permission
+export const basicRoutes = [
+  LoginRoute,
+  WelcomeRoute,
+  RootRoute,
+  ...mainOutRoutes,
+  REDIRECT_ROUTE,
+  PAGE_NOT_FOUND_ROUTE,
+];

+ 3 - 2
src/settings/componentSetting.ts

@@ -17,6 +17,7 @@ export default {
       // Total number of tables returned by the interface field name
       totalField: 'totalCount',
     },
+
     // Number of pages that can be selected
     pageSizeOptions: ['10', '20', '50', '80', '100'],
     // Default display quantity on one page
@@ -29,9 +30,9 @@ export default {
       if (field && order) {
         return {
           // The sort field passed to the backend you
-          field,
+          sidx: field,
           // Sorting method passed to the background asc/desc
-          order,
+          order: order.replace('end', ''), //用于适配后端的排序字段
         };
       } else {
         return {};

+ 29 - 29
src/settings/localeSetting.ts

@@ -1,29 +1,29 @@
-import type { DropMenu } from '../components/Dropdown';
-import type { LocaleSetting, LocaleType } from '/#/config';
-
-export const LOCALE: { [key: string]: LocaleType } = {
-  ZH_CN: 'zh_CN',
-  EN_US: 'en',
-};
-
-export const localeSetting: LocaleSetting = {
-  showPicker: false,
-  // Locale
-  locale: LOCALE.ZH_CN,
-  // Default locale
-  fallback: LOCALE.ZH_CN,
-  // available Locales
-  availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],
-};
-
-// locale list
-export const localeList: DropMenu[] = [
-  {
-    text: '简体中文',
-    event: LOCALE.ZH_CN,
-  },
-  {
-    text: 'English',
-    event: LOCALE.EN_US,
-  },
-];
+import type { DropMenu } from '../components/Dropdown';
+import type { LocaleSetting, LocaleType } from '/#/config';
+
+export const LOCALE: { [key: string]: LocaleType } = {
+  ZH_CN: 'zh_CN',
+  EN_US: 'en',
+};
+
+export const localeSetting: LocaleSetting = {
+  showPicker: false,
+  // Locale
+  locale: LOCALE.ZH_CN,
+  // Default locale
+  fallback: LOCALE.ZH_CN,
+  // available Locales
+  availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],
+};
+
+// locale list
+export const localeList: DropMenu[] = [
+  {
+    text: '简体中文',
+    event: LOCALE.ZH_CN,
+  },
+  {
+    text: 'English',
+    event: LOCALE.EN_US,
+  },
+];

+ 25 - 23
src/utils/dateUtil.ts

@@ -1,23 +1,25 @@
-/**
- * Independent time operation tool to facilitate subsequent switch to dayjs
- */
-import dayjs from 'dayjs';
-
-const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
-const DATE_FORMAT = 'YYYY-MM-DD';
-
-export function formatToDateTime(
-  date: dayjs.Dayjs | undefined = undefined,
-  format = DATE_TIME_FORMAT,
-): string {
-  return dayjs(date).format(format);
-}
-
-export function formatToDate(
-  date: dayjs.Dayjs | undefined = undefined,
-  format = DATE_FORMAT,
-): string {
-  return dayjs(date).format(format);
-}
-
-export const dateUtil = dayjs;
+/**
+ * Independent time operation tool to facilitate subsequent switch to dayjs
+ */
+import dayjs from 'dayjs';
+import duration from 'dayjs/plugin/duration';
+dayjs.extend(duration);
+
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
+const DATE_FORMAT = 'YYYY-MM-DD';
+
+export function formatToDateTime(
+  date: dayjs.Dayjs | undefined = undefined,
+  format = DATE_TIME_FORMAT,
+): string {
+  return dayjs(date).format(format);
+}
+
+export function formatToDate(
+  date: dayjs.Dayjs | undefined = undefined,
+  format = DATE_FORMAT,
+): string {
+  return dayjs(date).format(format);
+}
+
+export const dateUtil = dayjs;

+ 287 - 275
src/utils/http/axios/index.ts

@@ -1,275 +1,287 @@
-// axios配置  可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
-// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
-
-import type { AxiosResponse } from 'axios';
-import { clone } from 'lodash-es';
-import type { RequestOptions, Result } from '/#/axios';
-import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
-import { VAxios } from './Axios';
-import { checkStatus } from './checkStatus';
-import { useGlobSetting } from '/@/hooks/setting';
-import { useMessage } from '/@/hooks/web/useMessage';
-import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum';
-import { isString } from '/@/utils/is';
-import { getToken } from '/@/utils/auth';
-import { setObjToUrlParams, deepMerge } from '/@/utils';
-import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
-import { useI18n } from '/@/hooks/web/useI18n';
-import { joinTimestamp, formatRequestDate } from './helper';
-import { useUserStoreWithOut } from '/@/store/modules/user';
-
-const globSetting = useGlobSetting();
-const urlPrefix = globSetting.urlPrefix;
-const { createMessage, createErrorModal } = useMessage();
-
-/**
- * @description: 数据处理,方便区分多种处理方式
- */
-const transform: AxiosTransform = {
-  /**
-   * @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
-   */
-  transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
-    const { t } = useI18n();
-    const { isTransformResponse, isReturnNativeResponse } = options;
-    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
-    if (isReturnNativeResponse) {
-      return res;
-    }
-    // 不进行任何处理,直接返回
-    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
-    if (!isTransformResponse) {
-      return res.data;
-    }
-    // 错误的时候返回
-
-    const { data } = res;
-    if (!data) {
-      // return '[HTTP] Request has no return value';
-      throw new Error(t('sys.api.apiRequestFailed'));
-    }
-    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
-
-    const { code, error, message } = data;
-    // TODO
-    // 这里逻辑可以根据项目进行修改
-    const hasSuccess =
-      data &&
-      Reflect.has(data, 'code') &&
-      (code === ResultEnum.SUCCESS || code === ResultEnum.NORMAL);
-    if (hasSuccess) {
-      const converterResult = data.message;
-      const converterMessage = error;
-      Reflect.set(data, 'result', converterResult);
-      Reflect.set(data, 'message', converterMessage);
-      // data.result = converterResult;
-      // data.message = converterMessage;
-      delete data.error;
-      return data.result || data;
-    }
-
-    // 在此处根据自己项目的实际情况对不同的code执行不同的操作
-    // 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
-    let timeoutMsg = '';
-    switch (code) {
-      case ResultEnum.TIMEOUT:
-        timeoutMsg = t('sys.api.timeoutMessage');
-        const userStore = useUserStoreWithOut();
-        userStore.setToken(undefined);
-        userStore.logout(true);
-        break;
-      case ResultEnum.JAVA_ERROR:
-        if (error) {
-          timeoutMsg = error;
-        }
-        break;
-      default:
-        if (message) {
-          timeoutMsg = message;
-        }
-        //TODO 由于后端HACKCODE error当信息string
-        if (error) {
-          timeoutMsg = error;
-        }
-        break;
-    }
-
-    // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
-    // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
-    if (options.errorMessageMode === 'modal') {
-      createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg });
-    } else if (options.errorMessageMode === 'message') {
-      createMessage.error(timeoutMsg);
-    }
-    console.log('timeoutMsg', timeoutMsg);
-    throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'));
-  },
-
-  // 请求之前处理config
-  beforeRequestHook: (config, options) => {
-    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
-
-    if (joinPrefix) {
-      config.url = `${urlPrefix}${config.url}`;
-    }
-
-    if (apiUrl && isString(apiUrl)) {
-      config.url = `${apiUrl}${config.url}`;
-    }
-    const params = config.params || {};
-    const data = config.data || false;
-    formatDate && data && !isString(data) && formatRequestDate(data);
-    if (config.method?.toUpperCase() === RequestEnum.GET) {
-      if (!isString(params)) {
-        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
-        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
-      } else {
-        // 兼容restful风格
-        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
-        config.params = undefined;
-      }
-    } else {
-      if (!isString(params)) {
-        formatDate && formatRequestDate(params);
-        if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
-          config.data = data;
-          config.params = params;
-        } else {
-          // 非GET请求如果没有提供data,则将params视为data
-          config.data = params;
-          config.params = undefined;
-        }
-        if (joinParamsToUrl) {
-          config.url = setObjToUrlParams(
-            config.url as string,
-            Object.assign({}, config.params, config.data),
-          );
-        }
-      } else {
-        // 兼容restful风格
-        config.url = config.url + params;
-        config.params = undefined;
-      }
-    }
-    return config;
-  },
-
-  /**
-   * @description: 请求拦截器处理
-   */
-  requestInterceptors: (config, options) => {
-    // 请求之前处理config
-    const token = getToken();
-    if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
-      // jwt token
-      // (config as Recordable).headers.Authorization = options.authenticationScheme
-      //   ? `${options.authenticationScheme} ${token}`
-      //   : token;
-      (config as Recordable).headers.token = options.authenticationScheme
-        ? `${options.authenticationScheme} ${token}`
-        : token;
-    }
-    return config;
-  },
-
-  /**
-   * @description: 响应拦截器处理
-   */
-  responseInterceptors: (res: AxiosResponse<any>) => {
-    return res;
-  },
-
-  /**
-   * @description: 响应错误处理
-   */
-  responseInterceptorsCatch: (error: any) => {
-    const { t } = useI18n();
-    const errorLogStore = useErrorLogStoreWithOut();
-    errorLogStore.addAjaxErrorInfo(error);
-    const { response, code, message, config } = error || {};
-    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
-    const msg: string = response?.data?.error?.message ?? '';
-    const err: string = error?.toString?.() ?? '';
-    let errMessage = '';
-
-    try {
-      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
-        errMessage = t('sys.api.apiTimeoutMessage');
-      }
-      if (err?.includes('Network Error')) {
-        errMessage = t('sys.api.networkExceptionMsg');
-      }
-
-      if (errMessage) {
-        if (errorMessageMode === 'modal') {
-          createErrorModal({ title: t('sys.api.errorTip'), content: errMessage });
-        } else if (errorMessageMode === 'message') {
-          createMessage.error(errMessage);
-        }
-        return Promise.reject(error);
-      }
-    } catch (error) {
-      throw new Error(error as unknown as string);
-    }
-
-    checkStatus(error?.response?.status, msg, errorMessageMode);
-    return Promise.reject(error);
-  },
-};
-
-function createAxios(opt?: Partial<CreateAxiosOptions>) {
-  return new VAxios(
-    deepMerge(
-      {
-        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
-        // authentication schemes,e.g: Bearer
-        // authenticationScheme: 'Bearer',
-        authenticationScheme: '',
-        timeout: 10 * 1000,
-        // 基础接口地址
-        // baseURL: globSetting.apiUrl,
-
-        headers: { 'Content-Type': ContentTypeEnum.JSON },
-        // 如果是form-data格式
-        // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
-        // 数据处理方式
-        transform: clone(transform),
-        // 配置项,下面的选项都可以在独立的接口请求中覆盖
-        requestOptions: {
-          // 默认将prefix 添加到url
-          joinPrefix: true,
-          // 是否返回原生响应头 比如:需要获取响应头时使用该属性
-          isReturnNativeResponse: false,
-          // 需要对返回数据进行处理
-          isTransformResponse: true,
-          // post请求的时候添加参数到url
-          joinParamsToUrl: false,
-          // 格式化提交参数时间
-          formatDate: true,
-          // 消息提示类型
-          errorMessageMode: 'message',
-          // 接口地址
-          apiUrl: globSetting.apiUrl,
-          // 接口拼接地址
-          urlPrefix: urlPrefix,
-          //  是否加入时间戳
-          joinTime: true,
-          // 忽略重复请求
-          ignoreCancelToken: true,
-          // 是否携带token
-          withToken: true,
-        },
-      },
-      opt || {},
-    ),
-  );
-}
-export const defHttp = createAxios();
-
-// other api url
-// export const otherHttp = createAxios({
-//   requestOptions: {
-//     apiUrl: 'xxx',
-//     urlPrefix: 'xxx',
-//   },
-// });
+// axios配置  可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
+// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
+
+import type { AxiosResponse } from 'axios';
+import { clone } from 'lodash-es';
+import type { RequestOptions, Result } from '/#/axios';
+import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
+import { VAxios } from './Axios';
+import { checkStatus } from './checkStatus';
+import { useGlobSetting } from '/@/hooks/setting';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum';
+import { isString } from '/@/utils/is';
+import { getToken } from '/@/utils/auth';
+import { setObjToUrlParams, deepMerge } from '/@/utils';
+import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { joinTimestamp, formatRequestDate } from './helper';
+import { useUserStoreWithOut } from '/@/store/modules/user';
+
+const globSetting = useGlobSetting();
+const urlPrefix = globSetting.urlPrefix;
+const { createMessage, createErrorModal } = useMessage();
+
+/**
+ * @description: 数据处理,方便区分多种处理方式
+ */
+const transform: AxiosTransform = {
+  /**
+   * @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
+   */
+  transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
+    const { t } = useI18n();
+    const { isTransformResponse, isReturnNativeResponse } = options;
+    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+    if (isReturnNativeResponse) {
+      return res;
+    }
+    // 不进行任何处理,直接返回
+    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
+    if (!isTransformResponse) {
+      return res.data;
+    }
+    // 错误的时候返回
+
+    const { data } = res;
+    if (!data) {
+      // return '[HTTP] Request has no return value';
+      throw new Error(t('sys.api.apiRequestFailed'));
+    }
+    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
+
+    const { code, error, message } = data;
+    // TODO
+    // 这里逻辑可以根据项目进行修改
+    const hasSuccess =
+      data &&
+      Reflect.has(data, 'code') &&
+      (code === ResultEnum.SUCCESS || code === ResultEnum.NORMAL);
+    if (hasSuccess) {
+      const converterResult = data.message;
+      const converterMessage = error;
+      Reflect.set(data, 'result', converterResult);
+      Reflect.set(data, 'message', converterMessage);
+      // data.result = converterResult;
+      // data.message = converterMessage;
+      delete data.error;
+      //增加自定义result return
+      if (options.useResult) {
+        return data;
+      } else {
+        return data.result;
+      }
+    }
+
+    // 在此处根据自己项目的实际情况对不同的code执行不同的操作
+    // 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
+    let timeoutMsg = '';
+    switch (code) {
+      case ResultEnum.TIMEOUT:
+        timeoutMsg = t('sys.api.timeoutMessage');
+        const userStore = useUserStoreWithOut();
+        userStore.setToken(undefined);
+        userStore.logout(true);
+        break;
+      case ResultEnum.JAVA_ERROR:
+        if (error) {
+          timeoutMsg = error;
+        }
+        break;
+      default:
+        if (message) {
+          timeoutMsg = message;
+        }
+        //TODO 由于后端HACKCODE error当信息string
+        if (error) {
+          timeoutMsg = error;
+        }
+        break;
+    }
+
+    // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
+    // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
+    if (options.errorMessageMode === 'modal') {
+      createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg });
+    } else if (options.errorMessageMode === 'message') {
+      createMessage.error(timeoutMsg);
+    }
+    console.log('timeoutMsg', timeoutMsg);
+    if (options.useResult) {
+      return data;
+    }
+    if (res.config.responseType === 'blob') {
+      return data;
+    }
+
+    throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'));
+  },
+
+  // 请求之前处理config
+  beforeRequestHook: (config, options) => {
+    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
+
+    if (joinPrefix) {
+      config.url = `${urlPrefix}${config.url}`;
+    }
+
+    if (apiUrl && isString(apiUrl)) {
+      config.url = `${apiUrl}${config.url}`;
+    }
+    const params = config.params || {};
+    const data = config.data || false;
+    formatDate && data && !isString(data) && formatRequestDate(data);
+    if (config.method?.toUpperCase() === RequestEnum.GET) {
+      if (!isString(params)) {
+        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
+        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
+      } else {
+        // 兼容restful风格
+        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
+        config.params = undefined;
+      }
+    } else {
+      if (!isString(params)) {
+        formatDate && formatRequestDate(params);
+        if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
+          config.data = data;
+          config.params = params;
+        } else {
+          // 非GET请求如果没有提供data,则将params视为data
+          config.data = params;
+          config.params = undefined;
+        }
+        if (joinParamsToUrl) {
+          config.url = setObjToUrlParams(
+            config.url as string,
+            Object.assign({}, config.params, config.data),
+          );
+        }
+      } else {
+        // 兼容restful风格
+        config.url = config.url + params;
+        config.params = undefined;
+      }
+    }
+    return config;
+  },
+
+  /**
+   * @description: 请求拦截器处理
+   */
+  requestInterceptors: (config, options) => {
+    // 请求之前处理config
+    const token = getToken();
+    if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
+      // jwt token
+      // (config as Recordable).headers.Authorization = options.authenticationScheme
+      //   ? `${options.authenticationScheme} ${token}`
+      //   : token;
+      (config as Recordable).headers.token = options.authenticationScheme
+        ? `${options.authenticationScheme} ${token}`
+        : token;
+    }
+    return config;
+  },
+
+  /**
+   * @description: 响应拦截器处理
+   */
+  responseInterceptors: (res: AxiosResponse<any>) => {
+    return res;
+  },
+
+  /**
+   * @description: 响应错误处理
+   */
+  responseInterceptorsCatch: (error: any) => {
+    const { t } = useI18n();
+    const errorLogStore = useErrorLogStoreWithOut();
+    errorLogStore.addAjaxErrorInfo(error);
+    const { response, code, message, config } = error || {};
+    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
+    const msg: string = response?.data?.error?.message ?? '';
+    const err: string = error?.toString?.() ?? '';
+    let errMessage = '';
+
+    try {
+      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
+        errMessage = t('sys.api.apiTimeoutMessage');
+      }
+      if (err?.includes('Network Error')) {
+        errMessage = t('sys.api.networkExceptionMsg');
+      }
+
+      if (errMessage) {
+        if (errorMessageMode === 'modal') {
+          createErrorModal({ title: t('sys.api.errorTip'), content: errMessage });
+        } else if (errorMessageMode === 'message') {
+          createMessage.error(errMessage);
+        }
+        return Promise.reject(error);
+      }
+    } catch (error) {
+      throw new Error(error as unknown as string);
+    }
+
+    checkStatus(error?.response?.status, msg, errorMessageMode);
+    return Promise.reject(error);
+  },
+};
+
+function createAxios(opt?: Partial<CreateAxiosOptions>) {
+  return new VAxios(
+    deepMerge(
+      {
+        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
+        // authentication schemes,e.g: Bearer
+        // authenticationScheme: 'Bearer',
+        authenticationScheme: '',
+        timeout: 10 * 1000,
+        // 基础接口地址
+        // baseURL: globSetting.apiUrl,
+
+        headers: { 'Content-Type': ContentTypeEnum.JSON },
+        // 如果是form-data格式
+        // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
+        // 数据处理方式
+        transform: clone(transform),
+        // 配置项,下面的选项都可以在独立的接口请求中覆盖
+        requestOptions: {
+          // 默认将prefix 添加到url
+          joinPrefix: true,
+          // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+          isReturnNativeResponse: false,
+          // 需要对返回数据进行处理
+          isTransformResponse: true,
+          // post请求的时候添加参数到url
+          joinParamsToUrl: false,
+          // 格式化提交参数时间
+          formatDate: true,
+          // 消息提示类型
+          errorMessageMode: 'message',
+          // 接口地址
+          apiUrl: globSetting.apiUrl,
+          // 接口拼接地址
+          urlPrefix: urlPrefix,
+          //  是否加入时间戳
+          joinTime: true,
+          // 忽略重复请求
+          ignoreCancelToken: true,
+          // 是否携带token
+          withToken: true,
+        },
+      },
+      opt || {},
+    ),
+  );
+}
+export const defHttp = createAxios();
+
+// other api url
+// export const otherHttp = createAxios({
+//   requestOptions: {
+//     apiUrl: 'xxx',
+//     urlPrefix: 'xxx',
+//   },
+// });

+ 131 - 130
src/views/advertisement/list.vue

@@ -1,130 +1,131 @@
-<template>
-  <div class="p-4">
-    <BasicTable @register="registerTable">
-      <template #toolbar>
-        <a-button type="primary" @click="handleCreate">新增</a-button>
-      </template>
-      <template #time="{ record }">
-        <Time v-if="record.createTime" :value="record.createTime" mode="date" />
-      </template>
-      <template #img="{ record }">
-        <TableImg :size="200" :simpleShow="true" :imgList="[record.image]" />
-      </template>
-      <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              label: '编辑',
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              color: 'error',
-              label: '删除',
-              popConfirm: {
-                title: '是否确认删除',
-                confirm: deleteConfirm.bind(null, record),
-              },
-            },
-          ]"
-        />
-      </template>
-    </BasicTable>
-    <listDrawer @register="registerDrawer" @reload="reload" />
-  </div>
-</template>
-<script lang="ts">
-  import { defineComponent } from 'vue';
-  import { BasicTable, useTable, BasicColumn, TableAction, TableImg } from '/@/components/Table';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { useDrawer } from '/@/components/Drawer';
-  import { Time } from '/@/components/Time';
-  import listDrawer from './listDrawer.vue';
-  import { ListApi, itemDeletelApi } from '/@/api/advertisement/list';
-  // param type 2
-  export default defineComponent({
-    components: { BasicTable, TableAction, TableImg, listDrawer, Time },
-    setup() {
-      const { createMessage } = useMessage();
-      const [registerDrawer, { openDrawer }] = useDrawer();
-      const columns: BasicColumn[] = [
-        {
-          title: '序号',
-          dataIndex: 'id',
-          fixed: 'left',
-          width: 100,
-        },
-        {
-          title: '封面',
-          dataIndex: 'image',
-          ellipsis: true,
-          slots: { customRender: 'img' },
-          width: 230,
-        },
-        {
-          title: '标题',
-          dataIndex: 'title',
-          width: 230,
-        },
-        {
-          title: '排序',
-          dataIndex: 'orderNum',
-          width: 100,
-        },
-        {
-          title: '编辑时间',
-          dataIndex: 'createTime',
-          slots: { customRender: 'time' },
-          width: 150,
-        },
-
-        {
-          title: '操作',
-          dataIndex: '',
-          fixed: 'right',
-          slots: { customRender: 'action' },
-          width: 130,
-        },
-      ];
-
-      // { getForm }
-      const [registerTable, { reload }] = useTable({
-        title: '轮播图',
-        api: ListApi,
-        columns: columns,
-        useSearchForm: false,
-
-        showTableSetting: true,
-        tableSetting: { fullScreen: true },
-        showIndexColumn: false,
-        rowKey: 'id',
-      });
-
-      function handleCreate() {
-        openDrawer(true, {
-          isUpdate: false,
-        });
-      }
-      function handleEdit(record: Recordable) {
-        openDrawer(true, {
-          record,
-          isUpdate: true,
-        });
-      }
-      function deleteConfirm(val) {
-        itemDeletelApi({ id: val.id }).then((res) => {
-          createMessage.success(res);
-          reload();
-        });
-      }
-      return {
-        deleteConfirm,
-        reload,
-        registerTable,
-        createMessage,
-        registerDrawer,
-        handleCreate,
-        handleEdit,
-      };
-    },
-  });
-</script>
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate">新增</a-button>
+      </template>
+      <template #time="{ record }">
+        <Time v-if="record.createTime" :value="record.createTime" mode="date" />
+      </template>
+      <template #img="{ record }">
+        <TableImg :size="200" :simpleShow="true" :imgList="[record.image]" />
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: deleteConfirm.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <listDrawer @register="registerDrawer" @reload="reload" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { BasicTable, useTable, BasicColumn, TableAction, TableImg } from '/@/components/Table';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useDrawer } from '/@/components/Drawer';
+  import { Time } from '/@/components/Time';
+  import listDrawer from './listDrawer.vue';
+  import { ListApi, itemDeletelApi } from '/@/api/advertisement/list';
+  // param type 2
+  export default defineComponent({
+    components: { BasicTable, TableAction, TableImg, listDrawer, Time },
+    emits: ['register', 'reload'],
+    setup() {
+      const { createMessage } = useMessage();
+      const [registerDrawer, { openDrawer }] = useDrawer();
+      const columns: BasicColumn[] = [
+        {
+          title: '序号',
+          dataIndex: 'id',
+          fixed: 'left',
+          width: 100,
+        },
+        {
+          title: '封面',
+          dataIndex: 'image',
+          ellipsis: true,
+          slots: { customRender: 'img' },
+          width: 230,
+        },
+        {
+          title: '标题',
+          dataIndex: 'title',
+          width: 230,
+        },
+        {
+          title: '排序',
+          dataIndex: 'orderNum',
+          width: 100,
+        },
+        {
+          title: '编辑时间',
+          dataIndex: 'createTime',
+          slots: { customRender: 'time' },
+          width: 150,
+        },
+
+        {
+          title: '操作',
+          dataIndex: '',
+          fixed: 'right',
+          slots: { customRender: 'action' },
+          width: 130,
+        },
+      ];
+
+      // { getForm }
+      const [registerTable, { reload }] = useTable({
+        title: '轮播图',
+        api: ListApi,
+        columns: columns,
+        useSearchForm: false,
+
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: false,
+        rowKey: 'id',
+      });
+
+      function handleCreate() {
+        openDrawer(true, {
+          isUpdate: false,
+        });
+      }
+      function handleEdit(record: Recordable) {
+        openDrawer(true, {
+          record,
+          isUpdate: true,
+        });
+      }
+      function deleteConfirm(val) {
+        itemDeletelApi({ id: val.id }).then((res) => {
+          createMessage.success(res);
+          reload();
+        });
+      }
+      return {
+        deleteConfirm,
+        reload,
+        registerTable,
+        createMessage,
+        registerDrawer,
+        handleCreate,
+        handleEdit,
+      };
+    },
+  });
+</script>

+ 98 - 97
src/views/advertisement/listDrawer.vue

@@ -1,97 +1,98 @@
-<template>
-  <BasicDrawer
-    v-bind="$attrs"
-    width="50%"
-    :showDetailBack="false"
-    :title="getTitle"
-    @register="registerDrawer"
-  >
-    <BasicForm @register="registerForm" />
-  </BasicDrawer>
-</template>
-<script lang="ts">
-  import { defineComponent, ref, unref, computed } from 'vue';
-  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
-  import { BasicForm, useForm } from '/@/components/Form';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { schemas } from './schemas';
-  import { itemUpdateApi, itemSaveApi } from '/@/api/advertisement/list';
-  export default defineComponent({
-    components: { BasicDrawer, BasicForm },
-    setup(_, { emit }) {
-      const { createMessage } = useMessage();
-      const modelRef = ref({});
-      const isUpdate = ref(true);
-      const [registerForm, { validate, setProps, resetFields, setFieldsValue }] = useForm({
-        labelCol: {
-          span: 4,
-        },
-        wrapperCol: {
-          span: 18,
-        },
-        schemas: schemas,
-        actionColOptions: {
-          offset: 8,
-          span: 8,
-        },
-        submitButtonOptions: {
-          text: '提交',
-        },
-        submitFunc: summitAddDrawer,
-      });
-      const getTitle = computed(() => (!unref(isUpdate) ? '新增轮播资讯' : '编辑轮播资讯'));
-      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-        resetFields();
-        setDrawerProps({ confirmLoading: false });
-        isUpdate.value = !!data?.isUpdate;
-        modelRef.value = data.record;
-        if (unref(isUpdate)) {
-          console.log('data.record', data.record);
-          setFieldsValue({
-            ...data.record,
-            image: [data.record.image],
-          });
-        }
-      });
-
-      async function summitAddDrawer() {
-        try {
-          let data = await validate();
-          let httpApi = unref(isUpdate) ? itemUpdateApi : itemSaveApi;
-          setProps({
-            submitButtonOptions: {
-              loading: true,
-            },
-          });
-          console.log('data', data, modelRef.value);
-          httpApi({
-            ...modelRef.value,
-            ...data,
-            image: data.image[0],
-          }).then((res) => {
-            console.log('itemUpdateApi', res);
-            setProps({
-              submitButtonOptions: {
-                loading: false,
-              },
-            });
-            createMessage.success('提交成功!');
-            emit('reload');
-            closeDrawer();
-          });
-
-          // setTimeout(() => {
-
-          // }, 2000);
-        } catch (error) {}
-      }
-
-      return {
-        registerForm,
-        getTitle,
-        registerDrawer,
-        closeDrawer,
-      };
-    },
-  });
-</script>
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    width="50%"
+    :showDetailBack="false"
+    :title="getTitle"
+    @register="registerDrawer"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicDrawer>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, unref, computed } from 'vue';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { schemas } from './schemas';
+  import { itemUpdateApi, itemSaveApi } from '/@/api/advertisement/list';
+  export default defineComponent({
+    components: { BasicDrawer, BasicForm },
+    emits: ['register', 'reload'],
+    setup(_, { emit }) {
+      const { createMessage } = useMessage();
+      const modelRef = ref({});
+      const isUpdate = ref(true);
+      const [registerForm, { validate, setProps, resetFields, setFieldsValue }] = useForm({
+        labelCol: {
+          span: 4,
+        },
+        wrapperCol: {
+          span: 18,
+        },
+        schemas: schemas,
+        actionColOptions: {
+          offset: 8,
+          span: 8,
+        },
+        submitButtonOptions: {
+          text: '提交',
+        },
+        submitFunc: summitAddDrawer,
+      });
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增轮播资讯' : '编辑轮播资讯'));
+      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+        resetFields();
+        setDrawerProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+        modelRef.value = data.record;
+        if (unref(isUpdate)) {
+          console.log('data.record', data.record);
+          setFieldsValue({
+            ...data.record,
+            image: [data.record.image],
+          });
+        }
+      });
+
+      async function summitAddDrawer() {
+        try {
+          let data = await validate();
+          let httpApi = unref(isUpdate) ? itemUpdateApi : itemSaveApi;
+          setProps({
+            submitButtonOptions: {
+              loading: true,
+            },
+          });
+          console.log('data', data, modelRef.value);
+          httpApi({
+            ...modelRef.value,
+            ...data,
+            image: data.image[0],
+          }).then((res) => {
+            console.log('itemUpdateApi', res);
+            setProps({
+              submitButtonOptions: {
+                loading: false,
+              },
+            });
+            createMessage.success('提交成功!');
+            emit('reload');
+            closeDrawer();
+          });
+
+          // setTimeout(() => {
+
+          // }, 2000);
+        } catch (error) {}
+      }
+
+      return {
+        registerForm,
+        getTitle,
+        registerDrawer,
+        closeDrawer,
+      };
+    },
+  });
+</script>

+ 147 - 146
src/views/advertisement/pads.vue

@@ -1,146 +1,147 @@
-<template>
-  <div class="p-4">
-    <BasicTable @register="registerTable">
-      <template #toolbar>
-        <a-button type="primary" @click="handleCreate">新增</a-button>
-      </template>
-      <template #img="{ record }">
-        <TableImg :size="90" :simpleShow="true" :imgList="[record.image]" />
-      </template>
-      <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              label: '编辑',
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              color: 'error',
-              label: '删除',
-              popConfirm: {
-                title: '是否确认删除',
-                confirm: handleDelete.bind(null, record),
-              },
-            },
-          ]"
-        />
-      </template>
-    </BasicTable>
-    <PadsDrawer :type="type" @register="registerDrawer" @reload="reload" />
-  </div>
-</template>
-<script lang="ts">
-  import { defineComponent, h } from 'vue';
-  import { BasicTable, useTable, BasicColumn, TableAction, TableImg } from '/@/components/Table';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  // import { Tag } from 'ant-design-vue';
-  import PadsDrawer from './padsDrawer.vue';
-  import { RecommendListApi, RecommendDeleteApi } from '/@/api/advertisement/list';
-  import { useDrawer } from '/@/components/Drawer';
-  import { Time } from '/@/components/Time';
-  import { useRoute } from 'vue-router';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  // param type 2
-  export default defineComponent({
-    components: { BasicTable, TableAction, TableImg, PadsDrawer },
-    setup() {
-      const { t } = useI18n();
-      const { createMessage } = useMessage();
-      const [registerDrawer, { openDrawer }] = useDrawer();
-      const route = useRoute();
-      console.log('searchForm', route, route.fullPath);
-      const type = route.fullPath.split('/').pop(); //charAt(route.fullPath.length - 2) || '1';
-      const columns: BasicColumn[] = [
-        {
-          title: '排序',
-          dataIndex: 'orderNum',
-          fixed: 'left',
-          width: 100,
-        },
-        {
-          title: '封面',
-          dataIndex: 'image',
-          ellipsis: true,
-          slots: { customRender: 'img' },
-          width: 230,
-        },
-        {
-          title: '标题',
-          dataIndex: 'title',
-          width: 230,
-        },
-        {
-          title: '直播间链接',
-          dataIndex: 'sceneUrl',
-          width: 300,
-        },
-        {
-          title: '编辑时间',
-          dataIndex: 'createTime',
-          width: 150,
-          customRender: ({ record }) => {
-            return (
-              record.createTime &&
-              h(Time, {
-                value: record.createTime,
-                mode: 'datetime',
-              })
-            );
-          },
-        },
-
-        {
-          title: '操作',
-          dataIndex: '',
-          fixed: 'right',
-          slots: { customRender: 'action' },
-          width: 130,
-        },
-      ];
-
-      const [registerTable, { getForm, reload }] = useTable({
-        title: '推荐位',
-        api: RecommendListApi,
-        columns: columns,
-        // useSearchForm: true,
-        // formConfig: searchForm,
-        showTableSetting: true,
-        tableSetting: { fullScreen: true },
-        showIndexColumn: false,
-        rowKey: 'id',
-        searchInfo: {
-          type,
-        },
-      });
-      function handleCreate() {
-        openDrawer(true, {
-          isUpdate: false,
-          ...getForm().getFieldsValue(),
-        });
-      }
-      async function handleDelete(record: Recordable) {
-        let res = await RecommendDeleteApi({ id: record.id });
-        console.log(res);
-        createMessage.success(t('common.optSuccess'));
-        reload();
-      }
-      function handleEdit(record: Recordable) {
-        openDrawer(true, {
-          record,
-          ...getForm().getFieldsValue(),
-          isUpdate: true,
-        });
-      }
-      return {
-        registerTable,
-        createMessage,
-        registerDrawer,
-        handleCreate,
-        handleEdit,
-        reload,
-        type,
-        handleDelete,
-      };
-    },
-  });
-</script>
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate">新增</a-button>
+      </template>
+      <template #img="{ record }">
+        <TableImg :size="90" :simpleShow="true" :imgList="[record.image]" />
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <PadsDrawer :type="type" @register="registerDrawer" @reload="reload" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, h } from 'vue';
+  import { BasicTable, useTable, BasicColumn, TableAction, TableImg } from '/@/components/Table';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  // import { Tag } from 'ant-design-vue';
+  import PadsDrawer from './padsDrawer.vue';
+  import { RecommendListApi, RecommendDeleteApi } from '/@/api/advertisement/list';
+  import { useDrawer } from '/@/components/Drawer';
+  import { Time } from '/@/components/Time';
+  import { useRoute } from 'vue-router';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  // param type 2
+  export default defineComponent({
+    components: { BasicTable, TableAction, TableImg, PadsDrawer },
+    emits: ['register'],
+    setup() {
+      const { t } = useI18n();
+      const { createMessage } = useMessage();
+      const [registerDrawer, { openDrawer }] = useDrawer();
+      const route = useRoute();
+      console.log('searchForm', route, route.fullPath);
+      const type = route.fullPath.split('/').pop(); //charAt(route.fullPath.length - 2) || '1';
+      const columns: BasicColumn[] = [
+        {
+          title: '排序',
+          dataIndex: 'orderNum',
+          fixed: 'left',
+          width: 100,
+        },
+        {
+          title: '封面',
+          dataIndex: 'image',
+          ellipsis: true,
+          slots: { customRender: 'img' },
+          width: 230,
+        },
+        {
+          title: '标题',
+          dataIndex: 'title',
+          width: 230,
+        },
+        {
+          title: '直播间链接',
+          dataIndex: 'sceneUrl',
+          width: 300,
+        },
+        {
+          title: '编辑时间',
+          dataIndex: 'createTime',
+          width: 150,
+          customRender: ({ record }) => {
+            return (
+              record.createTime &&
+              h(Time, {
+                value: record.createTime,
+                mode: 'datetime',
+              })
+            );
+          },
+        },
+
+        {
+          title: '操作',
+          dataIndex: '',
+          fixed: 'right',
+          slots: { customRender: 'action' },
+          width: 130,
+        },
+      ];
+
+      const [registerTable, { getForm, reload }] = useTable({
+        title: '推荐位',
+        api: RecommendListApi,
+        columns: columns,
+        // useSearchForm: true,
+        // formConfig: searchForm,
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: false,
+        rowKey: 'id',
+        searchInfo: {
+          type,
+        },
+      });
+      function handleCreate() {
+        openDrawer(true, {
+          isUpdate: false,
+          ...getForm().getFieldsValue(),
+        });
+      }
+      async function handleDelete(record: Recordable) {
+        let res = await RecommendDeleteApi({ id: record.id });
+        console.log(res);
+        createMessage.success(t('common.optSuccess'));
+        reload();
+      }
+      function handleEdit(record: Recordable) {
+        openDrawer(true, {
+          record,
+          ...getForm().getFieldsValue(),
+          isUpdate: true,
+        });
+      }
+      return {
+        registerTable,
+        createMessage,
+        registerDrawer,
+        handleCreate,
+        handleEdit,
+        reload,
+        type,
+        handleDelete,
+      };
+    },
+  });
+</script>

+ 106 - 106
src/views/advertisement/padsDrawer.vue

@@ -1,106 +1,106 @@
-<template>
-  <BasicDrawer
-    v-bind="$attrs"
-    width="50%"
-    :showDetailBack="false"
-    :title="getTitle"
-    @register="registerDrawer"
-  >
-    <BasicForm @register="registerForm" />
-  </BasicDrawer>
-</template>
-<script lang="ts">
-  import { defineComponent, computed, ref, unref } from 'vue';
-  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
-  import { BasicForm, useForm } from '/@/components/Form';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { padsSchemas } from './schemas';
-  import { padsSaveApi, padsUpdateApi } from '/@/api/advertisement/list';
-
-  export default defineComponent({
-    components: { BasicDrawer, BasicForm },
-    props: {
-      type: {
-        type: String,
-        default: '0',
-      },
-    },
-    emits: ['reload'],
-    setup(props, { emit }) {
-      const { createMessage } = useMessage();
-      const modelRef = ref({
-        type: props.type,
-      });
-      const isUpdate = ref(true);
-      const [registerForm, { validate, setProps, resetFields, setFieldsValue }] = useForm({
-        labelCol: {
-          span: 4,
-        },
-        wrapperCol: {
-          span: 18,
-        },
-        schemas: padsSchemas,
-        actionColOptions: {
-          offset: 8,
-          span: 8,
-        },
-        submitButtonOptions: {
-          text: '提交',
-        },
-        submitFunc: summitAddDrawer,
-      });
-
-      async function summitAddDrawer() {
-        try {
-          let data = await validate();
-          let httpApi = unref(isUpdate) ? padsUpdateApi : padsSaveApi;
-          setProps({
-            submitButtonOptions: {
-              loading: true,
-            },
-          });
-          httpApi({
-            ...modelRef.value,
-            ...data,
-            image: data.image[0],
-          }).then((_) => {
-            setProps({
-              submitButtonOptions: {
-                loading: false,
-              },
-            });
-            createMessage.success('提交成功!');
-            emit('reload');
-            closeDrawer();
-          });
-        } catch (error) {
-          console.log('error', error);
-        }
-      }
-      const getTitle = computed(() => (!unref(isUpdate) ? '新增推荐位' : '编辑推荐位'));
-      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-        resetFields();
-        setDrawerProps({ confirmLoading: false });
-        isUpdate.value = !!data?.isUpdate;
-        modelRef.value = {
-          ...data.record,
-          type: props.type,
-        };
-
-        if (unref(isUpdate)) {
-          console.log('data.record', data.record);
-          setFieldsValue({
-            ...data.record,
-            image: [data.record.image],
-          });
-        }
-      });
-      return {
-        registerForm,
-        getTitle,
-        registerDrawer,
-        closeDrawer,
-      };
-    },
-  });
-</script>
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    width="50%"
+    :showDetailBack="false"
+    :title="getTitle"
+    @register="registerDrawer"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicDrawer>
+</template>
+<script lang="ts">
+  import { defineComponent, computed, ref, unref } from 'vue';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { padsSchemas } from './schemas';
+  import { padsSaveApi, padsUpdateApi } from '/@/api/advertisement/list';
+
+  export default defineComponent({
+    components: { BasicDrawer, BasicForm },
+    props: {
+      type: {
+        type: String,
+        default: '0',
+      },
+    },
+    emits: ['register', 'reload'],
+    setup(props, { emit }) {
+      const { createMessage } = useMessage();
+      const modelRef = ref({
+        type: props.type,
+      });
+      const isUpdate = ref(true);
+      const [registerForm, { validate, setProps, resetFields, setFieldsValue }] = useForm({
+        labelCol: {
+          span: 4,
+        },
+        wrapperCol: {
+          span: 18,
+        },
+        schemas: padsSchemas,
+        actionColOptions: {
+          offset: 8,
+          span: 8,
+        },
+        submitButtonOptions: {
+          text: '提交',
+        },
+        submitFunc: summitAddDrawer,
+      });
+
+      async function summitAddDrawer() {
+        try {
+          let data = await validate();
+          let httpApi = unref(isUpdate) ? padsUpdateApi : padsSaveApi;
+          setProps({
+            submitButtonOptions: {
+              loading: true,
+            },
+          });
+          httpApi({
+            ...modelRef.value,
+            ...data,
+            image: data.image[0],
+          }).then((_) => {
+            setProps({
+              submitButtonOptions: {
+                loading: false,
+              },
+            });
+            createMessage.success('提交成功!');
+            emit('reload');
+            closeDrawer();
+          });
+        } catch (error) {
+          console.log('error', error);
+        }
+      }
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增推荐位' : '编辑推荐位'));
+      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+        resetFields();
+        setDrawerProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+        modelRef.value = {
+          ...data.record,
+          type: props.type,
+        };
+
+        if (unref(isUpdate)) {
+          console.log('data.record', data.record);
+          setFieldsValue({
+            ...data.record,
+            image: [data.record.image],
+          });
+        }
+      });
+      return {
+        registerForm,
+        getTitle,
+        registerDrawer,
+        closeDrawer,
+      };
+    },
+  });
+</script>

+ 1 - 1
src/views/corporation/AddCorporationModal.vue

@@ -148,7 +148,7 @@
       label: t('routes.corporation.companyLogo'),
       required: true,
       rules: [{ required: true, message: '请选择上传文件' }],
-      helpMessage: '支持png  jpg图片格式',
+      helpMessage: '支持png jpg jpeg gif图片格式',
       itemProps: {
         validateTrigger: 'onBlur',
       },

+ 140 - 140
src/views/corporation/chargeModal.vue

@@ -1,140 +1,140 @@
-<template>
-  <BasicModal
-    v-bind="$attrs"
-    @register="register"
-    title="设备充值"
-    @ok="submit"
-    @visible-change="handleVisibleChange"
-  >
-    <div class="pt-3px pr-3px">
-      <BasicForm @register="registerForm" :model="modelRef">
-        <template #userName="{ record }">
-          {{ record.userName }}
-        </template>
-        <template #text="{ model, field }">
-          {{ model[field] }}
-        </template>
-      </BasicForm>
-    </div>
-  </BasicModal>
-</template>
-<script lang="ts">
-  import { defineComponent, ref, nextTick, inject } from 'vue';
-  import { BasicModal, useModalInner } from '/@/components/Modal';
-  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { addPoint } from '/@/api/corporation/modal';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  const schemas: FormSchema[] = [
-    {
-      field: 'userName',
-      component: 'Input',
-      label: '充值账号',
-      labelWidth: 100,
-      slot: 'text',
-      colProps: {
-        span: 20,
-      },
-    },
-    {
-      field: 'point',
-      component: 'Input',
-      slot: 'text',
-      label: '当前余额',
-      labelWidth: 100,
-      colProps: {
-        span: 20,
-      },
-      componentProps: {
-        disabled: true,
-      },
-    },
-    {
-      field: 'addPointnumber',
-      component: 'InputNumber',
-      label: '充 值',
-      labelWidth: 100,
-      colProps: {
-        span: 20,
-      },
-      defaultValue: 0,
-    },
-  ];
-  export default defineComponent({
-    components: { BasicModal, BasicForm },
-    props: {
-      userData: { type: Object },
-    },
-    emits: ['register', 'update'],
-    setup(props, { emit }) {
-      const { t } = useI18n();
-      const { createConfirm, createMessage } = useMessage();
-      const reload = inject('tablereload');
-      const modelRef = ref({
-        id: 0,
-      });
-      const [registerForm, { setFieldsValue, resetFields, getFieldsValue }] = useForm({
-        labelWidth: 120,
-        schemas,
-        showActionButtonGroup: false,
-        actionColOptions: {
-          span: 24,
-        },
-      });
-
-      const [register, { closeModal }] = useModalInner((data) => {
-        data && onDataReceive(data);
-      });
-      async function saveTable() {
-        let { addPointnumber } = getFieldsValue();
-        let res = await addPoint({
-          id: modelRef.value.id,
-          point: Number(addPointnumber),
-        });
-        console.log('saveTable', res, addPoint);
-        createMessage.success(t('common.optSuccess'));
-        // reload();
-        emit('update');
-        closeModal();
-      }
-      async function submit() {
-        createConfirm({
-          iconType: 'warning',
-          title: '提示',
-          content: '此操作将对该账号进行充值, 是否继续?',
-          onOk: async () => {
-            saveTable();
-          },
-        });
-      }
-      function onDataReceive(data) {
-        console.log('Data Received', data.record);
-        // 方式1;
-        resetFields();
-        setFieldsValue({
-          ...data.record,
-        });
-
-        // // 方式2
-        modelRef.value = {
-          ...data.record,
-        };
-      }
-
-      function handleVisibleChange(v) {
-        v && props.userData && nextTick(() => onDataReceive(props.userData));
-      }
-
-      return {
-        register,
-        submit,
-        saveTable,
-        reload,
-        schemas,
-        registerForm,
-        modelRef,
-        handleVisibleChange,
-      };
-    },
-  });
-</script>
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="register"
+    title="设备充值"
+    @ok="submit"
+    @visible-change="handleVisibleChange"
+  >
+    <div class="pt-3px pr-3px">
+      <BasicForm @register="registerForm" :model="modelRef">
+        <template #userName="{ record }">
+          {{ record.userName }}
+        </template>
+        <template #text="{ model, field }">
+          {{ model[field] }}
+        </template>
+      </BasicForm>
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, nextTick } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { addPoint } from '/@/api/corporation/modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  const schemas: FormSchema[] = [
+    {
+      field: 'userName',
+      component: 'Input',
+      label: '充值账号',
+      labelWidth: 100,
+      slot: 'text',
+      colProps: {
+        span: 20,
+      },
+    },
+    {
+      field: 'point',
+      component: 'Input',
+      slot: 'text',
+      label: '当前余额',
+      labelWidth: 100,
+      colProps: {
+        span: 20,
+      },
+      componentProps: {
+        disabled: true,
+      },
+    },
+    {
+      field: 'addPointnumber',
+      component: 'InputNumber',
+      label: '充 值',
+      labelWidth: 100,
+      colProps: {
+        span: 20,
+      },
+      defaultValue: 0,
+    },
+  ];
+  export default defineComponent({
+    components: { BasicModal, BasicForm },
+    props: {
+      userData: { type: Object },
+    },
+    emits: ['register', 'update'],
+    setup(props, { emit }) {
+      const { t } = useI18n();
+      const { createConfirm, createMessage } = useMessage();
+      // const reload = inject('tablereload');
+      const modelRef = ref({
+        id: 0,
+      });
+      const [registerForm, { setFieldsValue, resetFields, getFieldsValue, reload }] = useForm({
+        labelWidth: 120,
+        schemas,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 24,
+        },
+      });
+
+      const [register, { closeModal }] = useModalInner((data) => {
+        data && onDataReceive(data);
+      });
+      async function saveTable() {
+        let { addPointnumber } = getFieldsValue();
+        let res = await addPoint({
+          id: modelRef.value.id,
+          point: Number(addPointnumber),
+        });
+        console.log('saveTable', res, addPoint);
+        createMessage.success(t('common.optSuccess'));
+        // reload();
+        emit('update');
+        closeModal();
+      }
+      async function submit() {
+        createConfirm({
+          iconType: 'warning',
+          title: '提示',
+          content: '此操作将对该账号进行充值, 是否继续?',
+          onOk: async () => {
+            saveTable();
+          },
+        });
+      }
+      function onDataReceive(data) {
+        console.log('Data Received', data.record);
+        // 方式1;
+        resetFields();
+        setFieldsValue({
+          ...data.record,
+        });
+
+        // // 方式2
+        modelRef.value = {
+          ...data.record,
+        };
+      }
+
+      function handleVisibleChange(v) {
+        v && props.userData && nextTick(() => onDataReceive(props.userData));
+      }
+
+      return {
+        register,
+        submit,
+        saveTable,
+        reload,
+        schemas,
+        registerForm,
+        modelRef,
+        handleVisibleChange,
+      };
+    },
+  });
+</script>

+ 125 - 89
src/views/dashboard/analysis/components/VisitAnalysis.vue

@@ -1,89 +1,125 @@
-<template>
-  <div ref="chartRef" :style="{ height, width }"></div>
-</template>
-<script lang="ts">
-  import { basicProps } from './props';
-</script>
-<script lang="ts" setup>
-  import { onMounted, ref, Ref } from 'vue';
-  import { useECharts } from '/@/hooks/web/useECharts';
-
-  defineProps({
-    ...basicProps,
-  });
-  const chartRef = ref<HTMLDivElement | null>(null);
-  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
-
-  onMounted(() => {
-    setOptions({
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          lineStyle: {
-            width: 1,
-            color: '#019680',
-          },
-        },
-      },
-      xAxis: {
-        type: 'category',
-        boundaryGap: false,
-        data: [...new Array(18)].map((_item, index) => `${index + 6}:00`),
-        splitLine: {
-          show: true,
-          lineStyle: {
-            width: 1,
-            type: 'solid',
-            color: 'rgba(226,226,226,0.5)',
-          },
-        },
-        axisTick: {
-          show: false,
-        },
-      },
-      yAxis: [
-        {
-          type: 'value',
-          max: 80000,
-          splitNumber: 4,
-          axisTick: {
-            show: false,
-          },
-          splitArea: {
-            show: true,
-            areaStyle: {
-              color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
-            },
-          },
-        },
-      ],
-      grid: { left: '1%', right: '1%', top: '2  %', bottom: 0, containLabel: true },
-      series: [
-        {
-          smooth: true,
-          data: [
-            111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444, 22222,
-            11111, 4000, 2000, 500, 333, 222, 111,
-          ],
-          type: 'line',
-          areaStyle: {},
-          itemStyle: {
-            color: '#5ab1ef',
-          },
-        },
-        {
-          smooth: true,
-          data: [
-            33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201, 390,
-            198, 60, 30, 22, 11,
-          ],
-          type: 'line',
-          areaStyle: {},
-          itemStyle: {
-            color: '#019680',
-          },
-        },
-      ],
-    });
-  });
-</script>
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { basicProps } from './props';
+  // import { dateUtil } from '/@/utils/dateUtil';
+</script>
+<script lang="ts" setup>
+  import { ref, Ref, watch } from 'vue';
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  const props = defineProps({
+    ...basicProps,
+  });
+  const bulletChatAmountsData = ref<number[]>([]);
+  const userAmountData = ref<number[]>([]);
+  const yixStringData = ref<string[]>([]);
+  const maxSize = ref(0);
+
+  const chartRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+  function handleSetOptions() {
+    setOptions({
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          lineStyle: {
+            width: 1,
+            color: '#019680',
+          },
+        },
+      },
+      legend: {
+        orient: 'horizontal',
+        bottom: 0,
+      },
+      xAxis: {
+        type: 'category',
+        data: yixStringData.value,
+        splitLine: {
+          show: true,
+          lineStyle: {
+            width: 1,
+            type: 'solid',
+            color: 'rgba(226,226,226,0.5)',
+          },
+        },
+        axisTick: {
+          show: false,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          max: maxSize.value,
+          splitNumber: 4,
+          axisTick: {
+            show: false,
+          },
+          splitArea: {
+            show: true,
+            areaStyle: {
+              color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
+            },
+          },
+        },
+      ],
+      grid: { left: '2%', right: '2%', top: '2%', bottom: '10%', containLabel: true },
+      series: [
+        {
+          smooth: true,
+          data: userAmountData.value,
+          type: 'line',
+          areaStyle: {},
+          name: '留言总人数',
+          itemStyle: {
+            color: '#5ab1ef',
+          },
+        },
+        {
+          smooth: true,
+          data: bulletChatAmountsData.value,
+          type: 'line',
+          name: '留言总数',
+          areaStyle: {},
+          itemStyle: {
+            color: '#019680',
+          },
+        },
+      ],
+    });
+  }
+
+  watch(
+    () => [props.bulletChatAmounts, props.userAmount],
+    ([data1, data2]) => {
+      bulletChatAmountsData.value = data1.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.amount)),
+        [],
+      );
+
+      yixStringData.value = data1.reduce<string[]>(
+        (prev: string[], current) => prev.concat(current.date),
+        [],
+      );
+
+      userAmountData.value = data2.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.amount)),
+        [],
+      );
+      console.log('viewStatics-data', bulletChatAmountsData.value);
+      const maxNumber = Math.max(...bulletChatAmountsData.value.concat(userAmountData.value));
+      const pow = Math.pow(10, maxNumber.toString().length - 1);
+      maxSize.value = maxNumber > 10 ? Math.floor(maxNumber / 10) * 10 + pow * 2 : 10;
+      console.log('maxSize', maxSize.value);
+      handleSetOptions();
+      // viewStaticsData.value = data;
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+</script>

+ 97 - 47
src/views/dashboard/analysis/components/VisitAnalysisBar.vue

@@ -1,47 +1,97 @@
-<template>
-  <div ref="chartRef" :style="{ height, width }"></div>
-</template>
-<script lang="ts">
-  import { basicProps } from './props';
-</script>
-<script lang="ts" setup>
-  import { onMounted, ref, Ref } from 'vue';
-  import { useECharts } from '/@/hooks/web/useECharts';
-
-  defineProps({
-    ...basicProps,
-  });
-
-  const chartRef = ref<HTMLDivElement | null>(null);
-  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
-  onMounted(() => {
-    setOptions({
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          lineStyle: {
-            width: 1,
-            color: '#019680',
-          },
-        },
-      },
-      grid: { left: '1%', right: '1%', top: '2  %', bottom: 0, containLabel: true },
-      xAxis: {
-        type: 'category',
-        data: [...new Array(12)].map((_item, index) => `${index + 1}月`),
-      },
-      yAxis: {
-        type: 'value',
-        max: 8000,
-        splitNumber: 4,
-      },
-      series: [
-        {
-          data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800],
-          type: 'bar',
-          barMaxWidth: 80,
-        },
-      ],
-    });
-  });
-</script>
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { basicProps } from './props';
+  // import { dateUtil } from '/@/utils/dateUtil';
+</script>
+<script lang="ts" setup>
+  import { ref, Ref, watch } from 'vue';
+  // import type { dataItemType } from './props';
+  import { useECharts } from '/@/hooks/web/useECharts';
+  const props = defineProps({
+    ...basicProps,
+  });
+
+  const viewStaticsData = ref<number[]>([]);
+  const shareStaticsData = ref<number[]>([]);
+  const yixStringData = ref<string[]>([]);
+  const maxSize = ref(0);
+  const chartRef = ref<HTMLDivElement | null>(null);
+  const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+  function handlesetOptions() {
+    setOptions({
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          lineStyle: {
+            width: 1,
+            color: '#019680',
+          },
+        },
+      },
+      legend: {
+        orient: 'horizontal',
+        bottom: 0,
+      },
+      grid: { left: '2%', right: '2%', top: '2%', bottom: '10%', containLabel: true },
+      xAxis: {
+        type: 'category',
+        // data: [...new Array(30)].map((_item, index) => `${index + 1}日`),
+        data: yixStringData.value,
+      },
+      yAxis: {
+        type: 'value',
+        max: maxSize.value,
+        splitNumber: 4,
+      },
+      series: [
+        {
+          data: viewStaticsData.value,
+          type: 'bar',
+          itemStyle: { color: '#38a0ff' },
+          barMaxWidth: 80,
+          name: '用户浏览量',
+        },
+        {
+          data: shareStaticsData.value,
+          type: 'bar',
+          itemStyle: { color: '#4cca73' },
+          barMaxWidth: 80,
+          name: '用户分享数',
+        },
+      ],
+    });
+  }
+  // props.viewStatics,
+  watch(
+    () => [props.viewStatics, props.shareStatics],
+    ([data1, data2]) => {
+      console.log('viewStatics-data', data1);
+      viewStaticsData.value = data1.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.amount)),
+        [],
+      );
+
+      yixStringData.value = data1.reduce<string[]>(
+        (prev: string[], current) => prev.concat(current.date),
+        [],
+      );
+      shareStaticsData.value = data2.reduce<number[]>(
+        (prev: number[], current) => prev.concat(Number(current.amount)),
+        [],
+      );
+
+      const maxNumber = Math.max(...viewStaticsData.value.concat(shareStaticsData.value));
+      const pow = Math.pow(10, maxNumber.toString().length - 1);
+      maxSize.value = maxNumber > 10 ? Math.floor(maxNumber / 10) * 10 + pow * 2 : 10;
+      console.log('maxSize', maxSize.value);
+      handlesetOptions();
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+</script>

+ 37 - 16
src/views/dashboard/analysis/components/props.ts

@@ -1,16 +1,37 @@
-import { PropType } from 'vue';
-
-export interface BasicProps {
-  width: string;
-  height: string;
-}
-export const basicProps = {
-  width: {
-    type: String as PropType<string>,
-    default: '100%',
-  },
-  height: {
-    type: String as PropType<string>,
-    default: '280px',
-  },
-};
+import { PropType } from 'vue';
+
+export interface BasicProps {
+  width: string;
+  height: string;
+}
+export type dataItemType = {
+  date: string;
+  amount: string | number;
+};
+
+export const basicProps = {
+  width: {
+    type: String as PropType<string>,
+    default: '100%',
+  },
+  height: {
+    type: String as PropType<string>,
+    default: '280px',
+  },
+  viewStatics: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  shareStatics: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  bulletChatAmounts: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+  userAmount: {
+    type: Array as PropType<Array<dataItemType>>,
+    default: [],
+  },
+};

+ 278 - 0
src/views/dashboard/analysis/enterprise.vue

@@ -0,0 +1,278 @@
+<template>
+  <div class="p-4">
+    <!-- <GrowCard :loading="loading" class="enter-y" /> -->
+    <BasicTable @register="registerTable" :loading="loading">
+      <template #toolbar>
+        <a-button type="primary" @click="handleExport">导出数据</a-button>
+      </template>
+    </BasicTable>
+    <!-- <div class="md:flex enter-y">
+      <div class="md:w-1/2 enter-y">
+        <Card class="w-full">
+          <VisitAnalysisBar :loading="loading" />
+        </Card>
+      </div>
+      <div class="!md:mx-2"></div>
+      <Card class="md:w-1/2 enter-y">
+
+        <VisitAnalysis :loading="loading" />
+      </Card>
+    </div>
+    <div class="md:flex enter-y mt-4">
+      <Card class="md:w-1/2 md:w-full"> </Card>
+    </div> -->
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import { BasicTable, useTable, BasicColumn, FormProps } from '/@/components/Table';
+  // import { useI18n } from '/@/hooks/web/useI18n';
+  // import GrowCard from './components/GrowCard.vue';
+  import {
+    liveRoomStaticsApi,
+    // bulletChatStaticsApi,
+    companyChatExportApi,
+  } from '/@/api/dashboard/analysis';
+
+  import { ListApi as ListCorporationApi } from '/@/api/corporation/list';
+  import { listRoomsApi } from '/@/api/scene/list';
+  import { ListApi as ListLiveApi } from '/@/api/scene/live';
+
+  import { dateUtil, formatToDate } from '/@/utils/dateUtil';
+
+  const loading = ref(true);
+  // const { t } = useI18n();
+  const today = formatToDate(dateUtil(new Date()));
+  const priorDate = formatToDate(dateUtil(new Date().setDate(new Date().getDate() - 30)));
+
+  const columns: BasicColumn[] = [
+    {
+      title: '企业名称',
+      dataIndex: 'companyName',
+      width: 120,
+    },
+    {
+      title: '房间名称',
+      dataIndex: 'liveRoomName',
+      width: 120,
+    },
+    {
+      title: '直播场景名称',
+      dataIndex: 'brandName',
+      width: 120,
+    },
+    {
+      title: '主持人',
+      dataIndex: 'anchorName',
+      width: 120,
+    },
+    {
+      title: '总观看人数',
+      dataIndex: 'spectatorNum',
+      // sorter: true,
+      width: 120,
+    },
+    {
+      title: '总直播时长(min)',
+      dataIndex: 'duration',
+      // sorter: true,
+      width: 120,
+    },
+    {
+      title: '总分享数',
+      dataIndex: 'shareNum',
+      // sorter: true,
+      width: 120,
+    },
+  ];
+  const searchForm: Partial<FormProps> = {
+    labelWidth: 100,
+    schemas: [
+      {
+        field: 'companyId',
+        label: '请选择企业',
+        component: 'ApiSelect',
+        required: false,
+        componentProps: {
+          api: ListCorporationApi,
+          resultField: 'list',
+          labelField: 'name',
+          valueField: 'id',
+          immediate: false,
+          showSearch: true,
+          optionFilterProp: 'label',
+          // onChange: function (_, opt) {
+          //   // Reflect.set(modalRecord, 'shippingName', opt.label);
+          // },
+          params: {
+            page: 1,
+            limit: 1000,
+          },
+        },
+        colProps: {
+          lg: 24,
+          md: 24,
+          xl: 12,
+          xxl: 12,
+          // xl: 5,
+          // xxl: 12,
+        },
+      },
+      {
+        field: 'liveRoomId',
+        label: '请选择房间',
+        component: 'ApiSelect',
+        colProps: {
+          lg: 24,
+          md: 24,
+          xl: 12,
+          xxl: 12,
+        },
+        componentProps: {
+          api: listRoomsApi,
+          resultField: 'list',
+          labelField: 'name',
+          valueField: 'id',
+          immediate: false,
+          showSearch: true,
+          optionFilterProp: 'label',
+          // onChange: function (_, opt) {
+          //   // Reflect.set(modalRecord, 'shippingName', opt.label);
+          // },
+          params: {
+            page: 1,
+            limit: 1000,
+            state: 103,
+          },
+        },
+      },
+      {
+        field: 'brandId',
+        label: '请选择直播场景',
+        labelWidth: 130,
+        component: 'ApiSelect',
+        colProps: {
+          lg: 24,
+          md: 24,
+          xl: 12,
+          xxl: 12,
+        },
+        componentProps: {
+          api: ListLiveApi,
+          resultField: 'list',
+          labelField: 'name',
+          valueField: 'id',
+          immediate: false,
+          showSearch: true,
+          optionFilterProp: 'label',
+          params: {
+            page: 1,
+            limit: 1000,
+          },
+        },
+      },
+      {
+        field: 'time',
+        label: '时间段',
+        defaultValue: [priorDate, today],
+        component: 'RangePicker',
+        colProps: {
+          lg: 24,
+          md: 24,
+          xl: 12,
+          xxl: 12,
+        },
+        componentProps: {
+          format: 'YYYY-MM-DD',
+          // placeholder: ['开始时间', '结束时间'],
+          // showTime: { format: 'HH:mm:ss' },
+          disabledDate(current) {
+            // console.log('current', current, date);
+            return current && current > dateUtil().endOf('day');
+          },
+        },
+        rules: [
+          {
+            required: true,
+            // @ts-ignore
+            validator: async (rule, value) => {
+              if (!value) {
+                return Promise.reject('请选择时间段');
+              }
+              const days = Math.abs(value[1].diff(value[0], 'days'));
+              console.log('days', days);
+              if (days > 30) {
+                return Promise.reject('选择时间段应小于30天');
+              }
+              if (days < 1) {
+                return Promise.reject('至少选择2天以上');
+              }
+              return Promise.resolve();
+            },
+
+            trigger: 'change',
+          },
+        ],
+      },
+    ],
+    resetFunc: handleReset,
+  };
+
+  const searchInfo = ref({
+    liveRoomId: '',
+    companyId: '',
+    brandId: '',
+    limit: 20,
+    page: 1,
+    time: [priorDate, today],
+  });
+
+  const [registerTable] = useTable({
+    title: '',
+    columns: columns,
+    useSearchForm: true,
+    formConfig: searchForm,
+    api: liveRoomStaticsApi,
+    beforeFetch: (data) => {
+      console.log('beforeFetch', data);
+      data.time = data.time.map((item) => formatToDate(item));
+      searchInfo.value = data;
+      return data;
+    },
+    afterFetch: function (data) {
+      console.log('data', data);
+      return data;
+    },
+    searchInfo: searchInfo,
+    handleSearchInfoFn(data) {
+      searchInfo.value = Object.assign(searchInfo.value, data);
+      console.log(searchInfo.value);
+    },
+  });
+
+  async function handleExport() {
+    const data = await companyChatExportApi(searchInfo.value);
+    const downloadBlob = new Blob([data], {
+      type: 'application/msexcel',
+    });
+    const url = URL.createObjectURL(downloadBlob);
+    const a: HTMLAnchorElement = document.createElement('a');
+    document.body.appendChild(a);
+    a.style.display = 'none';
+    a.href = url;
+    const name = new Date().getTime();
+    a.download = `${name}.csv`;
+    a.click();
+    window.URL.revokeObjectURL(url);
+  }
+  async function handleReset() {
+    searchInfo.value = {
+      liveRoomId: '',
+      companyId: '',
+      brandId: '',
+      limit: 20,
+      page: 1,
+      time: [priorDate, today],
+    };
+  }
+</script>

+ 274 - 25
src/views/dashboard/analysis/index.vue

@@ -1,25 +1,274 @@
-<template>
-  <div class="p-4">
-    <GrowCard :loading="loading" class="enter-y" />
-    <SiteAnalysis class="!my-4 enter-y" :loading="loading" />
-    <div class="md:flex enter-y">
-      <VisitRadar class="md:w-1/3 w-full" :loading="loading" />
-      <VisitSource class="md:w-1/3 !md:mx-4 !md:my-0 !my-4 w-full" :loading="loading" />
-      <SalesProductPie class="md:w-1/3 w-full" :loading="loading" />
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { ref } from 'vue';
-  import GrowCard from './components/GrowCard.vue';
-  import SiteAnalysis from './components/SiteAnalysis.vue';
-  import VisitSource from './components/VisitSource.vue';
-  import VisitRadar from './components/VisitRadar.vue';
-  import SalesProductPie from './components/SalesProductPie.vue';
-
-  const loading = ref(true);
-
-  setTimeout(() => {
-    loading.value = false;
-  }, 1500);
-</script>
+<template>
+  <div class="p-4">
+    <!-- <GrowCard :loading="loading" class="enter-y" /> -->
+    <BasicTable @register="registerTable">
+      <template #headerTop>
+        <div class="md:flex enter-y">
+          <div class="md:w-1/2 enter-y">
+            <Card class="w-full">
+              <VisitAnalysisBar
+                :loading="loading"
+                :viewStatics="viewStaticsData"
+                :shareStatics="shareStaticsData"
+              />
+            </Card>
+          </div>
+          <div class="!md:mx-2"></div>
+          <Card class="md:w-1/2 enter-y">
+            <!-- <VisitAnalysis :loading="loading" /> -->
+            <VisitAnalysis
+              :loading="loading"
+              :bulletChatAmounts="bulletChatAmountsData"
+              :userAmount="userAmountData"
+            />
+          </Card>
+        </div>
+      </template>
+      <template #toolbar>
+        <a-button type="primary" @click="handleExport">导出数据</a-button>
+      </template>
+    </BasicTable>
+    <!-- <div class="md:flex enter-y">
+      <div class="md:w-1/2 enter-y">
+        <Card class="w-full">
+          <VisitAnalysisBar :loading="loading" />
+        </Card>
+      </div>
+      <div class="!md:mx-2"></div>
+      <Card class="md:w-1/2 enter-y">
+
+        <VisitAnalysis :loading="loading" />
+      </Card>
+    </div>
+    <div class="md:flex enter-y mt-4">
+      <Card class="md:w-1/2 md:w-full"> </Card>
+    </div> -->
+  </div>
+</template>
+<script lang="ts" setup>
+  import { onMounted, ref } from 'vue';
+  import { BasicTable, useTable, BasicColumn, FormProps } from '/@/components/Table';
+  // import { useI18n } from '/@/hooks/web/useI18n';
+  // import GrowCard from './components/GrowCard.vue';
+  import { Card } from 'ant-design-vue';
+  import VisitAnalysis from './components/VisitAnalysis.vue';
+  import VisitAnalysisBar from './components/VisitAnalysisBar.vue';
+  import {
+    bulletChatApi,
+    userStaticsApi,
+    bulletChatStaticsApi,
+    bulletChatExportApi,
+  } from '/@/api/dashboard/analysis';
+  import { listRoomsApi } from '/@/api/scene/list';
+  import { dateUtil, formatToDate } from '/@/utils/dateUtil';
+  import { StaticItemType } from '/@/api/dashboard/model';
+
+  const today = formatToDate(dateUtil(new Date()));
+  const priorDate = formatToDate(dateUtil(new Date().setDate(new Date().getDate() - 30)));
+
+  const loading = ref(true);
+  // UserStaticsModel
+
+  const viewStaticsData = ref<StaticItemType[]>([]);
+
+  const shareStaticsData = ref<StaticItemType[]>([]);
+
+  const bulletChatAmountsData = ref<StaticItemType[]>([]);
+  const userAmountData = ref<StaticItemType[]>([]);
+  // const { t } = useI18n();
+
+  const columns: BasicColumn[] = [
+    {
+      title: '微信昵称',
+      dataIndex: 'nickName',
+      width: 120,
+      ellipsis: true,
+      customRender: ({ record }) => {
+        const { nickName } = record;
+        return nickName?.length ? nickName : ' -- ';
+      },
+    },
+    {
+      title: '手机号',
+      dataIndex: 'userName',
+      width: 120,
+    },
+    {
+      title: '房间名称',
+      dataIndex: 'liveRoomName',
+      width: 120,
+    },
+    {
+      title: '在线时长(min)',
+      dataIndex: 'duration',
+      width: 120,
+    },
+    {
+      title: '留言条数',
+      dataIndex: 'bulletChatAmount',
+      width: 120,
+    },
+    {
+      title: '留言内容',
+      dataIndex: 'bulletChatContents',
+      width: 320,
+    },
+  ];
+  const searchForm: Partial<FormProps> = {
+    labelWidth: 100,
+    schemas: [
+      {
+        field: 'liveRoomId',
+        label: '全部房间',
+        component: 'ApiSelect',
+        // required: true,
+        colProps: {
+          xl: 5,
+          xxl: 5,
+        },
+        componentProps: {
+          api: listRoomsApi,
+          labelField: 'name',
+          resultField: 'list',
+          valueField: 'id',
+          immediate: false,
+          showSearch: true,
+          optionFilterProp: 'label',
+          params: {
+            page: 1,
+            limit: 1000,
+            state: 103,
+          },
+        },
+      },
+      {
+        field: 'time',
+        label: '时间段',
+        component: 'RangePicker',
+        defaultValue: [priorDate, today],
+        colProps: {
+          xl: 16,
+          xxl: 16,
+        },
+        componentProps: {
+          format: 'YYYY-MM-DD',
+          showTime: false,
+          disabledDate(current) {
+            // console.log('current', current, date);
+            return current && current > dateUtil().endOf('day');
+          },
+          // onChange: (value) => {
+          //   console.log('onchange', value);
+          // },
+        },
+        rules: [
+          {
+            required: true,
+            // @ts-ignore
+            validator: async (rule, value) => {
+              if (!value) {
+                return Promise.reject('请选择时间段');
+              }
+              const days = Math.abs(value[1].diff(value[0], 'days'));
+              console.log('days', days);
+              if (days > 30) {
+                return Promise.reject('选择时间段应小于30天');
+              }
+              if (days < 1) {
+                return Promise.reject('至少选择2天以上');
+              }
+              return Promise.resolve();
+            },
+
+            trigger: 'change',
+          },
+        ],
+      },
+    ],
+    resetFunc: handleReset,
+  };
+
+  const searchInfo = ref({
+    liveRoomId: '',
+    limit: 20,
+    page: 1,
+    time: [priorDate, today],
+  });
+
+  const [registerTable] = useTable({
+    title: '房间留言状态',
+    columns: columns,
+    useSearchForm: true,
+    formConfig: searchForm,
+    immediate: true,
+    api: bulletChatApi,
+    beforeFetch: (data) => {
+      console.log('beforeFetch', data);
+      data.time = data.time.map((item) => formatToDate(item));
+      handleStatic(data);
+      searchInfo.value = data;
+      return data;
+    },
+    afterFetch: function (data) {
+      console.log('afterFetch', arguments);
+      return data;
+    },
+    searchInfo: searchInfo,
+    handleSearchInfoFn(data) {
+      searchInfo.value = Object.assign(searchInfo.value, data);
+      console.log(searchInfo.value);
+      return;
+    },
+  });
+
+  onMounted(() => {
+    handleStatic();
+  });
+
+  async function handleStatic(search?: any) {
+    try {
+      loading.value = true;
+      const searchInfoParams = search || searchInfo.value;
+      const sData = await userStaticsApi(searchInfoParams);
+      const bData = await bulletChatStaticsApi(searchInfoParams);
+      console.log('sData', sData);
+      console.log('bData', sData);
+      viewStaticsData.value = sData.viewStatics;
+      shareStaticsData.value = sData.shareStatics;
+      userAmountData.value = bData.userAmount;
+      bulletChatAmountsData.value = bData.bulletChatAmounts;
+      loading.value = false;
+    } catch (error) {
+      loading.value = false;
+    }
+  }
+
+  async function handleExport() {
+    const data = await bulletChatExportApi(searchInfo.value);
+    const downloadBlob = new Blob([data], {
+      type: 'application/msexcel',
+    });
+    const url = URL.createObjectURL(downloadBlob);
+    const a: HTMLAnchorElement = document.createElement('a');
+    document.body.appendChild(a);
+    a.style.display = 'none';
+    a.href = url;
+    const name = new Date().getTime();
+    a.download = `${name}.csv`;
+    a.click();
+    window.URL.revokeObjectURL(url);
+  }
+
+  async function handleReset() {
+    searchInfo.value = {
+      liveRoomId: '',
+      limit: 20,
+      page: 1,
+      time: [priorDate, today],
+    };
+  }
+
+  // setTimeout(() => {
+  //   loading.value = false;
+  // }, 1500);
+</script>

+ 0 - 4
src/views/feedback/list.vue

@@ -130,10 +130,6 @@
         // rowKey: 'userId',
         pagination: { pageSize: 20 },
         bordered: true,
-        sortFn: (sortInfo) => {
-          let order = sortInfo.order && sortInfo.order.replace('end', '');
-          return { ...sortInfo, sidx: sortInfo.field, order: order };
-        },
         fetchSetting: {
           pageField: 'page',
           sizeField: 'limit',

+ 193 - 195
src/views/member/list.vue

@@ -1,195 +1,193 @@
-<!-- 
-avatar: "https://thirdwx.qlogo.cn/mmopen/vi_32/dTbT3X0Fm7BnUA4DoQhBy14smppF5eBibqPSOib3ou5elGuJ1eYZ9sib2ZALDMs1icYupWIxiaJwWETTV8we9brCHhQ/132"
-bindBrandId: null
-bindBrandName: null
-birthday: 631123200000
-brandId: null
-brandName: null
-city: ""
-gender: 1
-id: 25
-isAdmin: 0
-lastLoginIp: "117.136.32.65"
-lastLoginTime: 1631504271000
-levelName: null
-mobile: "13536501128"
-nickname: "波仔"
-password: "oeADe5U9uLeMYsNigq98zdu8J96A"
-registerIp: "117.136.32.65"
-registerTime: 1631504271000
-userLevelId: null
-username: "微信用户f8f4igi4j9ks"
-weixinOpenid: "oeADe5U9uLeMYsNigq98zdu8J96A" 
--->
-<template>
-  <div class="p-4">
-    <BasicTable @register="registerTable">
-      <template #toolbar> </template>
-      <template #avatar="{ record }">
-        <Avatar :size="80" :src="record.avatar" />
-      </template>
-
-      <template #gender="{ record }">
-        {{ renderGenderLabel(record.gender) }}
-      </template>
-
-      <template #birthday="{ record }">
-        <Time v-if="record.birthday" :value="record.birthday" mode="datetime" />
-      </template>
-
-      <template #lastLoginTime="{ record }">
-        <Time :value="record.lastLoginTime" mode="datetime" />
-      </template>
-
-      <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              label: '详情',
-              onClick: () => {
-                go(`/order/list/detail/${record.orderNo}`);
-              },
-            },
-            {
-              label: '打印',
-              color: 'error',
-              onClick: () => {
-                createMessage.info(`暂未接入`);
-              },
-            },
-          ]"
-        />
-      </template>
-    </BasicTable>
-  </div>
-</template>
-<script lang="ts">
-  import { defineComponent } from 'vue';
-  import { BasicTable, useTable, BasicColumn, FormProps, TableAction } from '/@/components/Table';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { uploadApi } from '/@/api/sys/upload';
-
-  import { ListApi } from '/@/api/member/list';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  // import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
-  import { useGo } from '/@/hooks/web/usePage';
-  import { Avatar } from 'ant-design-vue';
-  import { Time } from '/@/components/Time';
-
-  export default defineComponent({
-    components: { BasicTable, TableAction, Avatar, Time },
-    setup() {
-      const { createMessage } = useMessage();
-      const go = useGo();
-      const { t } = useI18n();
-      const columns: BasicColumn[] = [
-        {
-          title: 'ID',
-          dataIndex: 'id',
-          fixed: 'left',
-          width: 60,
-        },
-        {
-          title: '名称',
-          dataIndex: 'username',
-          // sorter: true,
-          width: 160,
-        },
-        {
-          title: '头像',
-          dataIndex: 'avatar',
-          slots: { customRender: 'avatar' },
-          width: 120,
-        },
-        {
-          title: '性别',
-          dataIndex: 'gender',
-          slots: { customRender: 'gender' },
-          // sorter: true,
-          width: 120,
-        },
-        {
-          title: '出生日期',
-          dataIndex: 'birthday',
-          slots: { customRender: 'birthday' },
-          width: 120,
-        },
-        {
-          title: '最后登录时间',
-          dataIndex: 'lastLoginTime',
-          slots: { customRender: 'lastLoginTime' },
-          // sorter: true,
-          width: 120,
-        },
-        {
-          title: '微信名',
-          dataIndex: 'nickname',
-          width: 120,
-        },
-        {
-          title: '手机号',
-          dataIndex: 'mobile',
-          width: 120,
-        },
-      ];
-
-      const searchForm: Partial<FormProps> = {
-        labelWidth: 100,
-        schemas: [
-          {
-            field: 'username',
-            label: '名称',
-            component: 'Input',
-            componentProps: {
-              maxLength: 100,
-            },
-            colProps: {
-              xl: 5,
-              xxl: 5,
-            },
-          },
-        ],
-      };
-
-      const [registerTable] = useTable({
-        title: '微信用户列表',
-        api: ListApi,
-        columns: columns,
-        useSearchForm: true,
-        formConfig: searchForm,
-        showTableSetting: true,
-        tableSetting: { fullScreen: true },
-        showIndexColumn: false,
-        rowKey: 'id',
-        pagination: { pageSize: 20 },
-        bordered: true,
-        fetchSetting: {
-          pageField: 'page',
-          sizeField: 'limit',
-          listField: 'list',
-          totalField: 'totalCount',
-        },
-      });
-
-      function renderGenderLabel(gender: number): string {
-        switch (gender) {
-          case 0:
-            return '女';
-          case 1:
-            return '男';
-          default:
-            return '未知';
-        }
-      }
-
-      return {
-        registerTable,
-        createMessage,
-        t,
-        go,
-        renderGenderLabel,
-        uploadApi: uploadApi as any,
-      };
-    },
-  });
-</script>
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <template #toolbar> </template>
+      <template #avatar="{ record }">
+        <Avatar :size="80" :src="record.avatar" />
+      </template>
+
+      <template #gender="{ record }">
+        {{ renderGenderLabel(record.gender) }}
+      </template>
+
+      <template #birthday="{ record }">
+        <Time v-if="record.birthday" :value="record.birthday" mode="datetime" />
+      </template>
+
+      <template #lastLoginTime="{ record }">
+        <Time :value="record.lastLoginTime" mode="datetime" />
+      </template>
+
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { BasicTable, useTable, BasicColumn, FormProps, TableAction } from '/@/components/Table';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi } from '/@/api/sys/upload';
+
+  import { ListApi, unbindWechatApi } from '/@/api/member/list';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  // import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { Avatar } from 'ant-design-vue';
+  import { Time } from '/@/components/Time';
+
+  export default defineComponent({
+    components: { BasicTable, TableAction, Avatar, Time },
+    setup() {
+      const { createMessage } = useMessage();
+      const go = useGo();
+      const { t } = useI18n();
+      const columns: BasicColumn[] = [
+        {
+          title: 'ID',
+          dataIndex: 'id',
+          fixed: 'left',
+          width: 60,
+        },
+        {
+          title: '名称',
+          dataIndex: 'username',
+          // sorter: true,
+          width: 160,
+        },
+        {
+          title: '头像',
+          dataIndex: 'avatar',
+          slots: { customRender: 'avatar' },
+          width: 120,
+        },
+        {
+          title: '性别',
+          dataIndex: 'gender',
+          slots: { customRender: 'gender' },
+          // sorter: true,
+          width: 120,
+        },
+        {
+          title: '出生日期',
+          dataIndex: 'birthday',
+          slots: { customRender: 'birthday' },
+          width: 120,
+        },
+        {
+          title: '最后登录时间',
+          dataIndex: 'lastLoginTime',
+          slots: { customRender: 'lastLoginTime' },
+          // sorter: true,
+          width: 120,
+        },
+        {
+          title: '微信名',
+          dataIndex: 'nickname',
+          width: 120,
+        },
+        {
+          title: '手机号',
+          dataIndex: 'mobile',
+          width: 120,
+        },
+        {
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          width: 50,
+        },
+      ];
+
+      const searchForm: Partial<FormProps> = {
+        labelWidth: 100,
+        schemas: [
+          {
+            field: 'username',
+            label: '名称',
+            component: 'Input',
+            componentProps: {
+              maxLength: 100,
+            },
+            colProps: {
+              xl: 5,
+              xxl: 5,
+            },
+          },
+          {
+            field: 'mobile',
+            label: '手机号',
+            component: 'Input',
+            componentProps: {
+              maxLength: 100,
+            },
+            colProps: {
+              xl: 5,
+              xxl: 5,
+            },
+          },
+        ],
+      };
+
+      const [registerTable, { reload }] = useTable({
+        title: '微信用户列表',
+        api: ListApi,
+        columns: columns,
+        useSearchForm: true,
+        formConfig: searchForm,
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: false,
+        rowKey: 'id',
+        pagination: { pageSize: 20 },
+        bordered: true,
+        fetchSetting: {
+          pageField: 'page',
+          sizeField: 'limit',
+          listField: 'list',
+          totalField: 'totalCount',
+        },
+      });
+
+      function renderGenderLabel(gender: number): string {
+        switch (gender) {
+          case 0:
+            return '女';
+          case 1:
+            return '男';
+          default:
+            return '未知';
+        }
+      }
+      async function handleDelete(record: Recordable) {
+        console.log('record', record);
+        try {
+          await unbindWechatApi(record.id);
+        } catch (error) {}
+        reload();
+      }
+
+      return {
+        registerTable,
+        createMessage,
+        t,
+        go,
+        renderGenderLabel,
+        uploadApi: uploadApi as any,
+        handleDelete,
+      };
+    },
+  });
+</script>

+ 319 - 0
src/views/notification/addModal.vue

@@ -0,0 +1,319 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @cancel="resetFields"
+    @register="register"
+    :title="title"
+    okText="立刻发布"
+    width="60%"
+    min-width="600px"
+    :height="550"
+    :showOkBtn="!isView"
+    @ok="handleOk"
+  >
+    <div class="pt-3px pr-3px">
+      <BasicForm @register="registerForm" :model="modelRef" />
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  // computed
+  import { defineComponent, h, ref } from 'vue';
+
+  import { addNoticeApi, editNoticeApi } from '/@/api/notification/list';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  // import { ListAllCompanyApi } from '/@/api/corporation/list';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  // import { useUserStore } from '/@/store/modules/user';
+  // import { Tinymce } from '/@/components/Tinymce';
+  import { uploadApi } from '/@/api/advertisement/list';
+  import { dateUtil, formatToDateTime } from '/@/utils/dateUtil';
+  import { TableImg } from '/@/components/Table';
+
+  const { t } = useI18n();
+  const now = dateUtil();
+
+  export default defineComponent({
+    components: { BasicModal, BasicForm },
+    props: {
+      userData: { type: Object },
+    },
+    emits: ['register', 'ok', 'cancel'],
+    setup(_, { emit }) {
+      const modelRef = ref({});
+      const isView = ref(false);
+      // const userStore = useUserStore();
+      // const { getCheckRole } = userStore;
+      // const userinfo = computed(() => userStore.getUserInfo);
+      // const { companyId } = userinfo.value;
+
+      const schemas: FormSchema[] = [
+        {
+          field: 'id',
+          component: 'Input',
+          label: 'id',
+          show: false,
+        },
+
+        {
+          field: 'title',
+          component: 'Input',
+          label: '消息标题',
+          required: true,
+          componentProps: {
+            maxlength: 10,
+            showCount: true,
+          },
+          colProps: {
+            span: 22,
+          },
+        },
+        {
+          field: 'selectTime',
+          component: 'RangePicker',
+          componentProps: {
+            format: 'YYYY-MM-DD HH:mm',
+            showTime: true,
+            disabledDate(current) {
+              return current && current < now;
+            },
+            disabledTime(_) {
+              // if (type === 'start') {
+              //   return {
+              //     // ...Array(dateUtil().minute()).keys()
+              //     disabledHours: () => [...Array(dateUtil().hour()).keys()],
+              //     disabledMinutes: () => [],
+              //     disabledSeconds: () => [],
+              //   };
+              // }
+              return {
+                disabledHours: () => [...Array(dateUtil().hour()).keys()],
+                disabledMinutes: () => [],
+                disabledSeconds: () => [],
+              };
+            },
+          },
+          label: '时间段',
+          required: true,
+
+          rules: [
+            {
+              required: true,
+              // @ts-ignore
+              validator: async (rule, value) => {
+                if (!value) {
+                  return Promise.reject('请选择时间段');
+                }
+                // console.log('check', rule, value, record);
+                // // console.log('value[1]', value[1].unix());
+                if (value[0].unix() > value[1].unix()) {
+                  return Promise.reject('开始时间要小于结束时间');
+                }
+                if (value[0].unix() > value[1].unix() - 300) {
+                  return Promise.reject('时间段最小5分钟');
+                }
+                return Promise.resolve();
+              },
+
+              trigger: 'change',
+            },
+          ],
+          colProps: {
+            span: 22,
+          },
+        },
+        {
+          field: 'type',
+          component: 'RadioButtonGroup',
+          label: '类型',
+          required: true,
+          colProps: {
+            span: 22,
+          },
+          defaultValue: 0,
+          componentProps: {
+            options: [
+              { label: '文字', value: 0 },
+              { label: '图片', value: 1 },
+            ],
+          },
+        },
+        {
+          field: 'content1',
+          label: '内容',
+          required: true,
+          component: 'InputTextArea',
+          ifShow: ({ model }) => {
+            return model['type'] === 0;
+          },
+          colProps: {
+            span: 22,
+          },
+          componentProps: {
+            showCount: true,
+            maxlength: 200,
+            autoSize: {
+              minRows: 6,
+            },
+          },
+        },
+        {
+          field: 'content2',
+          label: '内容',
+          component: 'Upload',
+          required: true,
+          helpMessage: '支持png,jpg和gif格式的图片,建议分辨率为200*400,最大为5MB',
+          itemProps: {
+            validateTrigger: 'blur',
+          },
+          ifShow: ({ model }) => {
+            return model['type'] === 1 && !isView.value;
+          },
+          componentProps: {
+            api: uploadApi,
+            maxSize: 5,
+            emptyHidePreview: true,
+            maxNumber: 1,
+            accept: ['jpg', 'jpeg', 'gif', 'png'],
+            helpText: '支持png,jpg和gif格式的图片,建议分辨率为200*400,最大为5MB',
+            afterFetch: function (data) {
+              Reflect.set(data, 'url', data.message.url);
+              return data;
+            },
+          },
+
+          colProps: {
+            span: 22,
+          },
+        },
+        {
+          field: 'content2Image',
+          component: 'Input',
+          required: true,
+          ifShow: ({ model }) => {
+            return model['type'] === 1 && isView.value;
+          },
+          render: ({ model, field }) => {
+            return h(TableImg, {
+              imgList: [model[field]],
+              size: 160,
+              style: {
+                height: 160,
+              },
+            });
+          },
+          label: '内容',
+          colProps: {
+            span: 10,
+          },
+        },
+      ];
+      const title = ref('创新消息');
+      const { createMessage } = useMessage();
+      const [registerForm, { validate, resetFields, setFieldsValue, updateSchema }] = useForm({
+        fieldMapToTime: [
+          // 支持多个字段
+          ['selectTime', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm'],
+        ],
+        labelWidth: 120,
+        schemas,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 24,
+        },
+      });
+      // closeModal
+      const [register, { closeModal }] = useModalInner((data) => {
+        data && onDataReceive(data);
+      });
+      async function onDataReceive(data) {
+        console.log('onDataReceive', data);
+        // 方式1;
+        resetFields();
+        isView.value = false;
+        let updateSchemas = schemas.map((item: FormSchema) => {
+          item.componentProps = {
+            ...item.componentProps,
+            disabled: false,
+          };
+          return item;
+        });
+        if (data.isView) {
+          isView.value = true;
+          title.value = '查看消息';
+          setFieldsValue({
+            ...data,
+          });
+          if (data.startTime && data.endTime) {
+            setFieldsValue({
+              selectTime: [formatToDateTime(data.startTime), formatToDateTime(data.endTime)],
+            });
+          }
+          if (data.type === 0) {
+            setFieldsValue({
+              content1: data.content,
+            });
+          }
+          if (data.type === 1) {
+            setFieldsValue({
+              content2: [data.content],
+              content2Image: [data.content],
+            });
+          }
+          updateSchemas = schemas.map((item: FormSchema) => {
+            item.componentProps = {
+              ...item.componentProps,
+              disabled: true,
+            };
+            return item;
+          });
+        } else {
+          title.value = '创建消息';
+        }
+        await updateSchema(updateSchemas);
+      }
+
+      async function handleOk() {
+        let data = await validate();
+        if (
+          data.selectTime[0].valueOf() < dateUtil().valueOf() ||
+          data.selectTime[1].valueOf() < dateUtil().valueOf()
+        ) {
+          createMessage.warn('开始或结束时间不能少于当前时间');
+          return;
+        }
+        console.log('data', data);
+
+        const requestApi = data.id ? editNoticeApi : addNoticeApi;
+        let res = await requestApi({
+          title: data.title,
+          content: data.type === 0 ? data.content1 : data.content2[0],
+          type: String(data.type),
+          effectiveTime: [
+            formatToDateTime(data.selectTime[0]),
+            formatToDateTime(data.selectTime[1]),
+          ],
+          id: data.id,
+        });
+        console.log('res', res);
+        emit('ok', res);
+        createMessage.success(t('common.optSuccess'));
+        closeModal();
+        resetFields();
+      }
+
+      return {
+        register,
+        title,
+        schemas,
+        registerForm,
+        modelRef,
+        handleOk,
+        resetFields,
+        isView,
+      };
+    },
+  });
+</script>

+ 169 - 0
src/views/notification/center.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <!-- <template #toolbar>
+        <a-button type="primary" @click="handleCreate">新增</a-button>
+      </template> -->
+      <template #avatar="{ record }">
+        <Avatar :size="80" :src="record.avatar" />
+      </template>
+
+      <template #content="{ record }">
+        <div v-if="record.type === 0" v-html="record.content"></div>
+        <!-- <img v-if="record.type === 1" :src="record.content" /> -->
+        <TableImg
+          v-if="record.type === 1"
+          :size="200"
+          :simpleShow="true"
+          :imgList="[record.content]"
+        />
+      </template>
+
+      <template #periodTime="{ record }">
+        <span v-if="record.startTime?.length && record.endTime?.length">
+          <Time :value="record.startTime" mode="datetime" /> 到
+          <Time :value="record.endTime" mode="datetime" />
+        </span>
+        <span v-else> -- </span>
+      </template>
+
+      <template #createTime="{ record }">
+        <Time :value="record.createTime" mode="datetime" />
+      </template>
+    </BasicTable>
+    <AddModal @register="registerAddModal" @ok="reload" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, onMounted } from 'vue';
+  import { BasicTable, useTable, BasicColumn, FormProps, TableImg } from '/@/components/Table';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi } from '/@/api/sys/upload';
+  import { useModal } from '/@/components/Modal';
+  import { listApi, deleteNoticeApi, readAllApi } from '/@/api/notification/list';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { Avatar } from 'ant-design-vue';
+  import { Time } from '/@/components/Time';
+  import AddModal from './addModal.vue';
+
+  export default defineComponent({
+    components: { BasicTable, Avatar, Time, AddModal, TableImg },
+    setup() {
+      const { createMessage } = useMessage();
+      const go = useGo();
+      const { t } = useI18n();
+      const [registerAddModal, { openModal: openAddModal }] = useModal();
+      const columns: BasicColumn[] = [
+        {
+          title: '序列',
+          dataIndex: 'id',
+          fixed: 'left',
+          width: 60,
+        },
+        {
+          title: '标题',
+          dataIndex: 'title',
+          // sorter: true,
+          width: 160,
+        },
+        {
+          title: '内容',
+          dataIndex: 'content',
+          slots: { customRender: 'content' },
+          // sorter: true,
+          width: 160,
+        },
+        {
+          title: '持续时间',
+          dataIndex: 'periodTime',
+          slots: { customRender: 'periodTime' },
+          // sorter: true,
+          width: 120,
+        },
+        {
+          title: '发布时间',
+          dataIndex: 'createTime',
+          slots: { customRender: 'createTime' },
+          // sorter: true,
+          width: 120,
+        },
+        // {
+        //   title: '操作',
+        //   dataIndex: '',
+        //   slots: { customRender: 'action' },
+        //   fixed: 'right',
+        //   width: 80,
+        // },
+      ];
+
+      const searchForm: Partial<FormProps> = {
+        labelWidth: 100,
+        schemas: [
+          {
+            field: 'title',
+            label: '标题',
+            component: 'Input',
+            componentProps: {
+              maxLength: 100,
+            },
+            colProps: {
+              xl: 5,
+              xxl: 5,
+            },
+          },
+        ],
+      };
+
+      const [registerTable, { reload }] = useTable({
+        title: '消息列表',
+        api: listApi,
+        columns: columns,
+        useSearchForm: true,
+        formConfig: searchForm,
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: false,
+        rowKey: 'id',
+        pagination: { pageSize: 20 },
+        bordered: true,
+        fetchSetting: {
+          pageField: 'page',
+          sizeField: 'limit',
+          listField: 'list',
+          totalField: 'totalCount',
+        },
+      });
+
+      function handleCreate(): void {
+        // openAddModal();
+        openAddModal(true, {});
+      }
+
+      onMounted(async () => {
+        await readAllApi();
+      });
+      async function handleDelete(record: Recordable) {
+        const data = await deleteNoticeApi({
+          id: record.id,
+        });
+        createMessage.success(t('common.optSuccess'));
+        reload();
+        console.log('data', data);
+      }
+
+      return {
+        registerTable,
+        createMessage,
+        t,
+        go,
+        uploadApi: uploadApi as any,
+        handleCreate,
+        reload,
+        registerAddModal,
+        openAddModal,
+        handleDelete,
+      };
+    },
+  });
+</script>

+ 197 - 0
src/views/notification/list.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate">新增</a-button>
+      </template>
+      <template #avatar="{ record }">
+        <Avatar :size="80" :src="record.avatar" />
+      </template>
+
+      <template #content="{ record }">
+        <div v-if="record.type === 0" v-html="record.content"></div>
+        <!-- <img v-if="record.type === 1" :src="record.content" /> -->
+        <TableImg
+          v-if="record.type === 1"
+          :size="200"
+          :simpleShow="true"
+          :imgList="[record.content]"
+        />
+      </template>
+
+      <template #periodTime="{ record }">
+        <span v-if="record.startTime?.length && record.endTime?.length">
+          <Time :value="record.startTime" mode="datetime" /> 到
+          <Time :value="record.endTime" mode="datetime" />
+        </span>
+        <span v-else> -- </span>
+      </template>
+
+      <template #createTime="{ record }">
+        <Time :value="record.createTime" mode="datetime" />
+      </template>
+
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              label: '查看',
+              onClick: () => {
+                handleView(record);
+              },
+            },
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <AddModal @register="registerAddModal" @ok="reload" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import {
+    BasicTable,
+    useTable,
+    BasicColumn,
+    FormProps,
+    TableAction,
+    TableImg,
+  } from '/@/components/Table';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi } from '/@/api/sys/upload';
+  import { useModal } from '/@/components/Modal';
+  import { listApi, deleteNoticeApi } from '/@/api/notification/list';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { Avatar } from 'ant-design-vue';
+  import { Time } from '/@/components/Time';
+  import AddModal from './addModal.vue';
+
+  export default defineComponent({
+    components: { BasicTable, TableAction, Avatar, Time, AddModal, TableImg },
+    setup() {
+      const { createMessage } = useMessage();
+      const go = useGo();
+      const { t } = useI18n();
+      const [registerAddModal, { openModal: openAddModal }] = useModal();
+      const columns: BasicColumn[] = [
+        {
+          title: '序列',
+          dataIndex: 'id',
+          fixed: 'left',
+          width: 60,
+        },
+        {
+          title: '标题',
+          dataIndex: 'title',
+          // sorter: true,
+          width: 160,
+        },
+        {
+          title: '内容',
+          dataIndex: 'content',
+          slots: { customRender: 'content' },
+          // sorter: true,
+          width: 160,
+        },
+        {
+          title: '持续时间',
+          dataIndex: 'periodTime',
+          slots: { customRender: 'periodTime' },
+          // sorter: true,
+          width: 120,
+        },
+        {
+          title: '发布时间',
+          dataIndex: 'createTime',
+          slots: { customRender: 'createTime' },
+          // sorter: true,
+          width: 120,
+        },
+        {
+          title: '操作',
+          dataIndex: '',
+          slots: { customRender: 'action' },
+          fixed: 'right',
+          width: 80,
+        },
+      ];
+
+      const searchForm: Partial<FormProps> = {
+        labelWidth: 100,
+        schemas: [
+          {
+            field: 'title',
+            label: '标题',
+            component: 'Input',
+            componentProps: {
+              maxLength: 100,
+            },
+            colProps: {
+              xl: 5,
+              xxl: 5,
+            },
+          },
+        ],
+      };
+
+      const [registerTable, { reload }] = useTable({
+        title: '消息列表',
+        api: listApi,
+        columns: columns,
+        useSearchForm: true,
+        formConfig: searchForm,
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: false,
+        rowKey: 'id',
+        pagination: { pageSize: 20 },
+        bordered: true,
+        fetchSetting: {
+          pageField: 'page',
+          sizeField: 'limit',
+          listField: 'list',
+          totalField: 'totalCount',
+        },
+      });
+
+      function handleCreate(): void {
+        // openAddModal();
+        openAddModal(true, { isView: false });
+      }
+      function handleView(record: Recordable): void {
+        openAddModal(true, { ...record, isView: true });
+      }
+      async function handleDelete(record: Recordable) {
+        const data = await deleteNoticeApi({
+          id: record.id,
+        });
+        createMessage.success(t('common.optSuccess'));
+        reload();
+        console.log('data', data);
+      }
+
+      return {
+        registerTable,
+        createMessage,
+        t,
+        go,
+        uploadApi: uploadApi as any,
+        handleCreate,
+        reload,
+        registerAddModal,
+        openAddModal,
+        handleDelete,
+        handleView,
+      };
+    },
+  });
+</script>

+ 5 - 0
src/views/notification/template.vue

@@ -0,0 +1,5 @@
+<template>
+  <div> 设备管理 </div>
+</template>
+
+<script lang="ts" setup></script>

+ 0 - 4
src/views/order/list.vue

@@ -302,10 +302,6 @@
           totalField: 'totalCount',
         },
         bordered: true,
-        sortFn: (sortInfo) => {
-          let order = sortInfo.order && sortInfo.order.replace('end', '');
-          return { ...sortInfo, sidx: sortInfo.field, order: order };
-        },
       });
 
       function renderOrderTypeLabel(type: number): string {

+ 116 - 116
src/views/product/addModal.vue

@@ -1,116 +1,116 @@
-<template>
-  <BasicModal
-    v-bind="$attrs"
-    @register="register"
-    @cancel="resetFields"
-    title="新 增"
-    @ok="handleOk"
-  >
-    <div class="pt-3px pr-3px">
-      <BasicForm @register="registerForm" :model="model" />
-    </div>
-  </BasicModal>
-</template>
-<script lang="ts">
-  import { defineComponent, ref } from 'vue';
-  import { saveItemApi } from '/@/api/product/category';
-  import { BasicModal, useModalInner } from '/@/components/Modal';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
-  const schemas: FormSchema[] = [
-    {
-      field: 'name',
-      component: 'Input',
-      label: '商品属性',
-      colProps: {
-        span: 18,
-      },
-      required: true,
-      componentProps: {
-        maxLength: 15,
-      },
-    },
-    {
-      field: 'sortOrder',
-      component: 'InputNumber',
-      label: '排序',
-      defaultValue: 1,
-      rules: [
-        {
-          required: false,
-          // @ts-ignore
-          validator: async (rule, value) => {
-            if (value && (value < 0 || value > 1000)) {
-              return Promise.reject('请输入正确的排序');
-            }
-            return Promise.resolve();
-          },
-          trigger: 'change',
-        },
-      ],
-      colProps: {
-        span: 24,
-      },
-    },
-  ];
-  export default defineComponent({
-    components: { BasicModal, BasicForm },
-    props: {
-      userData: { type: Object },
-    },
-    emits: ['saveadd'],
-    setup(_, context) {
-      const modelRef = ref({});
-      const { createMessage } = useMessage();
-      const { t } = useI18n();
-      const [
-        registerForm,
-        {
-          // setFieldsValue,
-          validate,
-          resetFields,
-          // getFieldsValue,
-        },
-      ] = useForm({
-        labelWidth: 120,
-        schemas,
-        showActionButtonGroup: false,
-        actionColOptions: {
-          span: 24,
-        },
-      });
-
-      const [register, { closeModal }] = useModalInner((data) => {
-        data && onDataReceive(data);
-      });
-
-      function onDataReceive(data) {
-        console.log('Data Received', data);
-        // 方式1;
-        // setFieldsValue({
-        //   field2: data.data,
-        //   field1: data.info,
-        // });
-
-        // // 方式2
-        modelRef.value = { field2: data.data, field1: data.info };
-
-        // setProps({
-        //   model:{ field2: data.data, field1: data.info }
-        // })
-      }
-
-      async function handleOk() {
-        let data = await validate();
-        let res = await saveItemApi(data);
-        context && context.emit('saveadd', res);
-        createMessage.success(t('common.optSuccess'));
-        closeModal();
-        resetFields();
-      }
-
-      return { register, resetFields, schemas, registerForm, model: modelRef, handleOk, t };
-    },
-  });
-</script>
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="register"
+    @cancel="resetFields"
+    title="新 增"
+    @ok="handleOk"
+  >
+    <div class="pt-3px pr-3px">
+      <BasicForm @register="registerForm" :model="model" />
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import { saveItemApi } from '/@/api/product/category';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  const schemas: FormSchema[] = [
+    {
+      field: 'name',
+      component: 'Input',
+      label: '商品属性',
+      colProps: {
+        span: 18,
+      },
+      required: true,
+      componentProps: {
+        maxLength: 15,
+      },
+    },
+    {
+      field: 'sortOrder',
+      component: 'InputNumber',
+      label: '排序',
+      defaultValue: 1,
+      rules: [
+        {
+          required: false,
+          // @ts-ignore
+          validator: async (rule, value) => {
+            if (value && (value < 0 || value > 1000)) {
+              return Promise.reject('请输入正确的排序');
+            }
+            return Promise.resolve();
+          },
+          trigger: 'change',
+        },
+      ],
+      colProps: {
+        span: 24,
+      },
+    },
+  ];
+  export default defineComponent({
+    components: { BasicModal, BasicForm },
+    props: {
+      userData: { type: Object },
+    },
+    emits: ['register', 'saveadd'],
+    setup(_, context) {
+      const modelRef = ref({});
+      const { createMessage } = useMessage();
+      const { t } = useI18n();
+      const [
+        registerForm,
+        {
+          // setFieldsValue,
+          validate,
+          resetFields,
+          // getFieldsValue,
+        },
+      ] = useForm({
+        labelWidth: 120,
+        schemas,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 24,
+        },
+      });
+
+      const [register, { closeModal }] = useModalInner((data) => {
+        data && onDataReceive(data);
+      });
+
+      function onDataReceive(data) {
+        console.log('Data Received', data);
+        // 方式1;
+        // setFieldsValue({
+        //   field2: data.data,
+        //   field1: data.info,
+        // });
+
+        // // 方式2
+        modelRef.value = { field2: data.data, field1: data.info };
+
+        // setProps({
+        //   model:{ field2: data.data, field1: data.info }
+        // })
+      }
+
+      async function handleOk() {
+        let data = await validate();
+        let res = await saveItemApi(data);
+        context && context.emit('saveadd', res);
+        createMessage.success(t('common.optSuccess'));
+        closeModal();
+        resetFields();
+      }
+
+      return { register, resetFields, schemas, registerForm, model: modelRef, handleOk, t };
+    },
+  });
+</script>

+ 171 - 170
src/views/product/category.vue

@@ -1,170 +1,171 @@
-<template>
-  <div>
-    <BasicTable @register="registerTable">
-      <template #toolbar v-if="!getCheckRole('tourist')">
-        <a-button primary color="error" @click="handleCreate"> 新增商品分类</a-button>
-        <!-- <a-button ghost color="warning" @click="expandAll">展开全部</a-button>
-        <a-button type="primary" @click="collapseAll">折叠全部</a-button> -->
-      </template>
-      <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              label: '编辑',
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              color: 'error',
-              label: '删除',
-              popConfirm: {
-                title: '是否确认删除',
-                confirm: handleDelete.bind(null, record),
-              },
-            },
-          ]"
-        />
-      </template>
-    </BasicTable>
-    <AddCategoryModal @register="registeraddCategoryModal" @update="reload" />
-    <EditCategoryModal @register="registerEditCategoryModal" @update="reload" />
-  </div>
-</template>
-
-<script lang="ts">
-  import { defineComponent, h, nextTick } from 'vue';
-  import { BasicTable, useTable, BasicColumn, TableAction } from '/@/components/Table';
-  import { categoryApi, deleteCategoryApi } from '/@/api/product/category';
-  import { Tag } from 'ant-design-vue';
-
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { makeTree } from '/@/utils/treeUtils';
-  import { useUserStore } from '/@/store/modules/user';
-
-  import AddCategoryModal from './addCategoryModal.vue';
-  import EditCategoryModal from './editCategoryModal.vue';
-  import { useModal } from '/@/components/Modal';
-
-  export default defineComponent({
-    components: { BasicTable, TableAction, AddCategoryModal, EditCategoryModal },
-    setup() {
-      const { createMessage } = useMessage();
-      const userStore = useUserStore();
-      const { getCheckRole } = userStore;
-      const { t } = useI18n();
-
-      const columns: BasicColumn[] = [
-        {
-          title: 'ID',
-          dataIndex: 'id',
-          fixed: 'left',
-          width: 100,
-        },
-        {
-          title: '分类名称',
-          dataIndex: 'name',
-          width: 200,
-          align: 'left',
-        },
-        {
-          title: '排序',
-          dataIndex: 'sortOrder',
-          width: 50,
-        },
-        {
-          title: '级数',
-          dataIndex: 'level',
-          width: 50,
-        },
-        {
-          title: '状态',
-          dataIndex: 'isShow',
-          width: 80,
-          customRender: ({ record }) => {
-            const status = record.isShow;
-            const enable = status === 1;
-            const color = enable ? 'green' : 'red';
-            const text = enable ? '显示' : '不显示';
-            return h(Tag, { color: color }, () => text);
-          },
-        },
-        // {
-        //   title: '创建时间',
-        //   dataIndex: 'createTime',
-        //   width: 180,
-        // },
-      ];
-      const [registeraddCategoryModal, { openModal: openAddCategoryModal }] = useModal();
-      const [registerEditCategoryModal, { openModal: openEditategoryModal }] = useModal();
-      const [registerTable, { reload, expandAll, collapseAll }] = useTable({
-        title: '商品分类',
-        api: categoryApi,
-        columns: columns,
-        isTreeTable: true,
-        useSearchForm: true,
-        formConfig: {
-          labelWidth: 100,
-          schemas: [
-            {
-              field: 'name',
-              label: '分类名称',
-              component: 'Input',
-              componentProps: {
-                placeholder: '请输入分类名称',
-                maxLength: 100,
-              },
-              colProps: {
-                xl: 6,
-                xxl: 6,
-              },
-            },
-          ],
-        },
-        showTableSetting: true,
-        tableSetting: { fullScreen: true },
-        showIndexColumn: true,
-        pagination: false,
-        rowKey: 'id',
-        bordered: true,
-        actionColumn: {
-          ifShow: !getCheckRole('tourist'),
-          width: 100,
-          title: '操作',
-          dataIndex: 'action',
-          slots: { customRender: 'action' },
-          fixed: undefined,
-        },
-        afterFetch(data) {
-          const treeData = makeTree(data);
-          return treeData;
-        },
-      });
-
-      function handleCreate(record: Recordable) {
-        openAddCategoryModal(true, record);
-      }
-      function handleEdit(record: Recordable) {
-        openEditategoryModal(true, record);
-      }
-      async function handleDelete(record: Recordable) {
-        await deleteCategoryApi([record.id]);
-        createMessage.success(t('common.optSuccess'));
-        nextTick(reload);
-      }
-      return {
-        registerTable,
-        createMessage,
-        t,
-        handleCreate,
-        handleEdit,
-        handleDelete,
-        expandAll,
-        collapseAll,
-        registeraddCategoryModal,
-        registerEditCategoryModal,
-        reload,
-        getCheckRole,
-      };
-    },
-  });
-</script>
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #toolbar v-if="!getCheckRole('tourist')">
+        <a-button primary color="error" @click="handleCreate"> 新增商品分类</a-button>
+        <!-- <a-button ghost color="warning" @click="expandAll">展开全部</a-button>
+        <a-button type="primary" @click="collapseAll">折叠全部</a-button> -->
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              label: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <AddCategoryModal @register="registeraddCategoryModal" @update="reload" />
+    <EditCategoryModal @register="registerEditCategoryModal" @update="reload" />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, h, nextTick } from 'vue';
+  import { BasicTable, useTable, BasicColumn, TableAction } from '/@/components/Table';
+  import { categoryApi, deleteCategoryApi } from '/@/api/product/category';
+  import { Tag } from 'ant-design-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { makeTree } from '/@/utils/treeUtils';
+  import { useUserStore } from '/@/store/modules/user';
+
+  import AddCategoryModal from './addCategoryModal.vue';
+  import EditCategoryModal from './editCategoryModal.vue';
+  import { useModal } from '/@/components/Modal';
+
+  export default defineComponent({
+    components: { BasicTable, TableAction, AddCategoryModal, EditCategoryModal },
+    emits: ['register'],
+    setup() {
+      const { createMessage } = useMessage();
+      const userStore = useUserStore();
+      const { getCheckRole } = userStore;
+      const { t } = useI18n();
+
+      const columns: BasicColumn[] = [
+        {
+          title: 'ID',
+          dataIndex: 'id',
+          fixed: 'left',
+          width: 100,
+        },
+        {
+          title: '分类名称',
+          dataIndex: 'name',
+          width: 200,
+          align: 'left',
+        },
+        {
+          title: '排序',
+          dataIndex: 'sortOrder',
+          width: 50,
+        },
+        {
+          title: '级数',
+          dataIndex: 'level',
+          width: 50,
+        },
+        {
+          title: '状态',
+          dataIndex: 'isShow',
+          width: 80,
+          customRender: ({ record }) => {
+            const status = record.isShow;
+            const enable = status === 1;
+            const color = enable ? 'green' : 'red';
+            const text = enable ? '显示' : '不显示';
+            return h(Tag, { color: color }, () => text);
+          },
+        },
+        // {
+        //   title: '创建时间',
+        //   dataIndex: 'createTime',
+        //   width: 180,
+        // },
+      ];
+      const [registeraddCategoryModal, { openModal: openAddCategoryModal }] = useModal();
+      const [registerEditCategoryModal, { openModal: openEditategoryModal }] = useModal();
+      const [registerTable, { reload, expandAll, collapseAll }] = useTable({
+        title: '商品分类',
+        api: categoryApi,
+        columns: columns,
+        isTreeTable: true,
+        useSearchForm: true,
+        formConfig: {
+          labelWidth: 100,
+          schemas: [
+            {
+              field: 'name',
+              label: '分类名称',
+              component: 'Input',
+              componentProps: {
+                placeholder: '请输入分类名称',
+                maxLength: 100,
+              },
+              colProps: {
+                xl: 6,
+                xxl: 6,
+              },
+            },
+          ],
+        },
+        showTableSetting: true,
+        tableSetting: { fullScreen: true },
+        showIndexColumn: true,
+        pagination: false,
+        rowKey: 'id',
+        bordered: true,
+        actionColumn: {
+          ifShow: !getCheckRole('tourist'),
+          width: 100,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          fixed: undefined,
+        },
+        afterFetch(data) {
+          const treeData = makeTree(data);
+          return treeData;
+        },
+      });
+
+      function handleCreate(record: Recordable) {
+        openAddCategoryModal(true, record);
+      }
+      function handleEdit(record: Recordable) {
+        openEditategoryModal(true, record);
+      }
+      async function handleDelete(record: Recordable) {
+        await deleteCategoryApi([record.id]);
+        createMessage.success(t('common.optSuccess'));
+        nextTick(reload);
+      }
+      return {
+        registerTable,
+        createMessage,
+        t,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        expandAll,
+        collapseAll,
+        registeraddCategoryModal,
+        registerEditCategoryModal,
+        reload,
+        getCheckRole,
+      };
+    },
+  });
+</script>

+ 3 - 0
src/views/product/drawer.data.ts

@@ -21,6 +21,7 @@ export const formSchema: FormSchema[] = [
         return treeData;
       },
       showSearch: true,
+      optionFilterProp: 'label',
       filterTreeNode: (searchVal, treeNode) => {
         return treeNode.title.includes(searchVal);
       },
@@ -70,6 +71,8 @@ export const formSchema: FormSchema[] = [
       labelField: 'name',
       valueField: 'id',
       immediate: true,
+      showSearch: true,
+      optionFilterProp: 'label',
       params: {
         page: 1,
         limit: 1000,

+ 2 - 1
src/views/product/goodsSpecs.vue

@@ -73,7 +73,7 @@
         default: () => {},
       },
     },
-    emits: ['update', 'editdata'],
+    emits: ['update', 'editdata', 'register'],
     setup(props, { emit }) {
       let ggList = reactive({
         goodsNumber: 1,
@@ -109,6 +109,7 @@
                 value: 'id',
               },
               showSearch: true,
+              optionFilterProp: 'label',
               filterTreeNode: (searchVal, treeNode) => {
                 return treeNode.title.includes(searchVal);
               },

+ 1 - 0
src/views/product/list.data.ts

@@ -60,6 +60,7 @@ export const columns: BasicColumn[] = [
     title: '购买链接',
     dataIndex: 'realShopUrl',
     slots: { customRender: 'realShopUrl' },
+    ellipsis: true,
     width: 150,
   },
   {

+ 1 - 4
src/views/product/list.vue

@@ -84,6 +84,7 @@
 
   export default defineComponent({
     components: { BasicTable, TableAction, ProductDrawer, Time, PopConfirmButton },
+    emits: ['register'],
     setup() {
       const { createMessage } = useMessage();
       const go = useGo();
@@ -110,10 +111,6 @@
           listField: 'list',
           totalField: 'totalCount',
         },
-        sortFn: (sortInfo) => {
-          let order = sortInfo.order && sortInfo.order.replace('end', '');
-          return { ...sortInfo, sidx: sortInfo.field, order: order };
-        },
       });
 
       function handleEdit(record: Recordable) {

+ 347 - 347
src/views/product/productDrawer.vue

@@ -1,347 +1,347 @@
-<template>
-  <BasicDrawer
-    v-bind="$attrs"
-    @register="registerDrawer"
-    :isDetail="true"
-    :showDetailBack="false"
-    showFooter
-    :title="getTitle"
-    @ok="handleSubmit"
-  >
-    <BasicForm @register="registerForm">
-      <template #goodsgg>
-        <goodsSpecs :key="editData.id" :editdata="editData" @update="setEditData" ref="goodsRef" />
-      </template>
-      <template #detailed>
-        <table class="detailed">
-          <thead>
-            <tr style="" v-show="editData.productList.length > 0">
-              <th style="min-width: 60px">{{ editData.categoryName }}</th>
-              <th>库存</th>
-              <th>规格编码</th>
-              <th>市场价</th>
-              <th>销售价</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr v-for="item in editData.productList" :key="item.uuidLink">
-              <template v-if="editData.productList">
-                <td
-                  :title="item.name"
-                  style="
-                    max-width: 120px;
-                    overflow: hidden;
-                    white-space: nowrap;
-                    text-overflow: ellipsis;
-                  "
-                  >{{ item.name }}</td
-                >
-                <td
-                  ><InputNumber
-                    type="number"
-                    :maxlength="8"
-                    :max="100000000"
-                    :min="0"
-                    v-model:value="item.goodsNumber"
-                /></td>
-                <td
-                  ><Input
-                    type="text"
-                    :maxlength="8"
-                    :max="100000000"
-                    :min="0"
-                    v-model:value="item.goodsSn"
-                /></td>
-                <td
-                  ><InputNumber
-                    type="number"
-                    :maxlength="8"
-                    :max="100000000"
-                    :min="0"
-                    v-model:value="item.marketPrice"
-                /></td>
-                <td
-                  ><InputNumber
-                    type="number"
-                    :maxlength="8"
-                    :max="100000000"
-                    :min="0"
-                    v-model:value="item.retailPrice"
-                /></td>
-              </template>
-            </tr>
-          </tbody>
-        </table>
-      </template>
-      <!-- <template #formItem>
-        <BasicForm @register="itemRegister" @submit="handleSubmit">
-          <template #add="{ field }">
-            <a-button v-if="Number(field) === 0" @click="add">+</a-button>
-            <a-button v-if="field > 0" @click="del.bind(null, field)">-</a-button>
-          </template>
-        </BasicForm>
-      </template> -->
-    </BasicForm>
-  </BasicDrawer>
-</template>
-<script lang="ts">
-  import { defineComponent, ref, computed, unref } from 'vue';
-  import { BasicForm, useForm } from '/@/components/Form/index';
-  import { Input, InputNumber } from 'ant-design-vue';
-  import { formSchema } from './drawer.data';
-  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
-  import goodsSpecs from './goodsSpecs.vue';
-  import { InfoApi, UpdateSaleApi, SaveSaleApi } from '/@/api/product/list';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { useUserStore } from '/@/store/modules/user';
-  // import { getMenuList } from '/@/api/system/system';
-  import { makeTree, TreeNode } from '/@/utils/treeUtils';
-  import { categoryApi } from '/@/api/product/category';
-
-  interface EditDataType {
-    categoryName: string;
-    productList: ProductListItem[];
-    goodsSpecificationList?: goodsSpecificationList[];
-    id: number;
-  }
-  interface goodsSpecificationList {
-    brandName: string;
-    goodsId?: number;
-    goodsName: string;
-    id?: number;
-    picUrl: string;
-    specificationId?: number;
-    specificationName: string;
-    uuid: string;
-    val?: string | number | [];
-    value: string;
-  }
-  interface ProductListItem {
-    uuidLink: string;
-    goodsNumber?: string;
-    goodsSn: string;
-    retailPrice: string;
-    marketPrice: string;
-    name?: string;
-  }
-
-  export default defineComponent({
-    name: 'ProductDrawer',
-    components: { BasicDrawer, BasicForm, goodsSpecs, Input, InputNumber },
-    emits: ['reload', 'register'],
-    setup(_, { emit }) {
-      const isUpdate = ref(true);
-      const goodsRef = ref();
-      const { createMessage } = useMessage();
-      const userStore = useUserStore();
-      const userinfo = computed(() => userStore.getUserInfo);
-      const { t } = useI18n();
-
-      let editData = ref<EditDataType>({
-        categoryName: '',
-        productList: [],
-        id: 0,
-      });
-
-      const [registerForm, { resetFields, updateSchema, setFieldsValue, validate }] = useForm({
-        labelWidth: 120,
-        schemas: formSchema,
-        showActionButtonGroup: false,
-        baseColProps: { lg: 18, md: 18, offset: 1 },
-      });
-      const [itemRegister, {}] = useForm({
-        // formItemValue :validate appendSchemaByField, removeSchemaByFiled,
-        schemas: [
-          {
-            field: 'goodsId',
-            label: '规格名',
-            component: 'ApiTreeSelect',
-            componentProps: {
-              api: async (params) => {
-                const res = (await categoryApi(params)) as any as TreeNode[];
-                const treeData = makeTree(res);
-                return treeData;
-              },
-              maxLength: 15,
-              fieldNames: {
-                label: 'name',
-                key: 'id',
-                value: 'id',
-              },
-            },
-          },
-          {
-            field: 'goodsSn',
-            component: 'Input',
-            label: '字段0',
-            colProps: {
-              span: 8,
-            },
-            required: true,
-          },
-          {
-            field: '0',
-            component: 'Input',
-            label: ' ',
-            colProps: {
-              span: 8,
-            },
-            slot: 'add',
-          },
-        ],
-        labelWidth: 100,
-        actionColOptions: { span: 24 },
-      });
-      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-        resetFields();
-        setDrawerProps({ confirmLoading: false });
-        isUpdate.value = !!data?.isUpdate;
-        editData.value = {
-          categoryName: '',
-          productList: [],
-          id: 0,
-        };
-        updateSchema({
-          field: 'brandId',
-          componentProps: {
-            disabled: unref(isUpdate) ? true : false,
-          },
-        });
-        if (unref(isUpdate)) {
-          let res = await InfoApi(data.record.id);
-          editData.value = {
-            ...res,
-            productList: res.productList.map((ele) => {
-              console.log('editData', ele, res.goodsSpecificationList);
-              ele.name = res.goodsSpecificationList.find(
-                (element) => element.uuid == ele.uuidLink,
-              )?.specificationName;
-              return ele;
-            }),
-          };
-          setFieldsValue({
-            ...res,
-            isOnSale: res.isOnSale === 1,
-            listPicUrl:
-              res.listPicUrl && res.listPicUrl.split('#$#').map((ele) => ele.split('?')[0]),
-            primaryPicUrl:
-              res.primaryPicUrl && res.primaryPicUrl.split('#$#').map((ele) => ele.split('?')[0]),
-            // primaryPicUrl: res.primaryPicUrl && [encodeURI(res.primaryPicUrl.split('?')[0])],
-            steamRoom: data.record?.steamRoom?.name,
-          });
-        }
-      });
-      function setEditData(value) {
-        let productList = editData.value.productList || [];
-        value.productList.map((ele) => {
-          if (!productList.some((element) => ele.uuidLink == element.uuidLink)) {
-            //判断新增
-            productList.push({
-              uuidLink: ele.uuidLink,
-              name: ele.name,
-              goodsNumber: '',
-              goodsSn: '',
-              marketPrice: '',
-              retailPrice: '',
-            });
-          } else {
-            //本来就有的 修改赋值
-            productList.forEach((setEle) => {
-              if (ele.uuidLink == setEle.uuidLink) {
-                setEle.name = ele.name;
-              }
-            });
-          }
-        });
-        productList = productList.filter((ele) =>
-          value.productList.some((element) => element.uuidLink == ele.uuidLink),
-        );
-        console.log('setEditData', productList);
-        editData.value = {
-          ...editData.value,
-          ...value,
-          productList,
-        };
-        setFieldsValue({
-          productList: productList,
-        });
-      }
-      const getTitle = computed(() => (!unref(isUpdate) ? '新增商品' : '编辑商品'));
-
-      async function handleSubmit() {
-        const { companyId, id } = userinfo.value;
-        try {
-          const values = await validate();
-          console.log('values', values);
-          let apiData = {
-            ...editData.value,
-            ...values,
-            retailPrice:
-              editData.value.productList &&
-              Math.min(...editData.value.productList.map((ele) => Number(ele.retailPrice))),
-            marketPrice:
-              editData.value.productList &&
-              Math.min(...editData.value.productList.map((ele) => Number(ele.marketPrice))),
-            isOnSale: values.isOnSale ? 1 : 0,
-          };
-
-          let requresApi = isUpdate.value ? UpdateSaleApi : SaveSaleApi;
-          if (!isUpdate.value) {
-            apiData.createUserDeptId = companyId;
-            apiData.createUserId = id;
-          }
-          let subData = await requresApi({
-            ...apiData,
-            primaryPicUrl: apiData.primaryPicUrl.join('#$#'),
-            listPicUrl: apiData.listPicUrl.join('#$#'),
-          });
-          console.log('setFieldsValue', subData);
-          setDrawerProps({ confirmLoading: true });
-          // TODO custom api
-          console.log(values);
-          createMessage.success(t('common.optSuccess'));
-          closeDrawer();
-          emit('reload');
-        } finally {
-          setDrawerProps({ confirmLoading: false });
-        }
-      }
-
-      return {
-        goodsRef,
-        registerDrawer,
-        registerForm,
-        itemRegister,
-        getTitle,
-        editData,
-        handleSubmit,
-        setEditData,
-      };
-    },
-  });
-</script>
-
-<style scoped lang="less">
-  :deep(.detailed) {
-    .title {
-      display: flex;
-      span,
-      input,
-      div {
-        flex-grow: 1;
-      }
-      input {
-        display: inline-block;
-      }
-      .name {
-        flex-grow: 2;
-      }
-    }
-    tr {
-      th {
-        font-weight: 500;
-      }
-    }
-  }
-</style>
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    @register="registerDrawer"
+    :isDetail="true"
+    :showDetailBack="false"
+    showFooter
+    :title="getTitle"
+    @ok="handleSubmit"
+  >
+    <BasicForm @register="registerForm">
+      <template #goodsgg>
+        <goodsSpecs :key="editData.id" :editdata="editData" @update="setEditData" ref="goodsRef" />
+      </template>
+      <template #detailed>
+        <table class="detailed">
+          <thead>
+            <tr style="" v-show="editData.productList.length > 0">
+              <th style="min-width: 60px">{{ editData.categoryName }}</th>
+              <th>库存</th>
+              <th>规格编码</th>
+              <th>市场价</th>
+              <th>销售价</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in editData.productList" :key="item.uuidLink">
+              <template v-if="editData.productList">
+                <td
+                  :title="item.name"
+                  style="
+                    max-width: 120px;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                  "
+                  >{{ item.name }}</td
+                >
+                <td
+                  ><InputNumber
+                    type="number"
+                    :maxlength="8"
+                    :max="100000000"
+                    :min="0"
+                    v-model:value="item.goodsNumber"
+                /></td>
+                <td
+                  ><Input
+                    type="text"
+                    :maxlength="8"
+                    :max="100000000"
+                    :min="0"
+                    v-model:value="item.goodsSn"
+                /></td>
+                <td
+                  ><InputNumber
+                    type="number"
+                    :maxlength="8"
+                    :max="100000000"
+                    :min="0"
+                    v-model:value="item.marketPrice"
+                /></td>
+                <td
+                  ><InputNumber
+                    type="number"
+                    :maxlength="8"
+                    :max="100000000"
+                    :min="0"
+                    v-model:value="item.retailPrice"
+                /></td>
+              </template>
+            </tr>
+          </tbody>
+        </table>
+      </template>
+      <!-- <template #formItem>
+        <BasicForm @register="itemRegister" @submit="handleSubmit">
+          <template #add="{ field }">
+            <a-button v-if="Number(field) === 0" @click="add">+</a-button>
+            <a-button v-if="field > 0" @click="del.bind(null, field)">-</a-button>
+          </template>
+        </BasicForm>
+      </template> -->
+    </BasicForm>
+  </BasicDrawer>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, computed, unref } from 'vue';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { Input, InputNumber } from 'ant-design-vue';
+  import { formSchema } from './drawer.data';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import goodsSpecs from './goodsSpecs.vue';
+  import { InfoApi, UpdateSaleApi, SaveSaleApi } from '/@/api/product/list';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useUserStore } from '/@/store/modules/user';
+  // import { getMenuList } from '/@/api/system/system';
+  import { makeTree, TreeNode } from '/@/utils/treeUtils';
+  import { categoryApi } from '/@/api/product/category';
+
+  interface EditDataType {
+    categoryName: string;
+    productList: ProductListItem[];
+    goodsSpecificationList?: goodsSpecificationList[];
+    id: number;
+  }
+  interface goodsSpecificationList {
+    brandName: string;
+    goodsId?: number;
+    goodsName: string;
+    id?: number;
+    picUrl: string;
+    specificationId?: number;
+    specificationName: string;
+    uuid: string;
+    val?: string | number | [];
+    value: string;
+  }
+  interface ProductListItem {
+    uuidLink: string;
+    goodsNumber?: string;
+    goodsSn: string;
+    retailPrice: string;
+    marketPrice: string;
+    name?: string;
+  }
+
+  export default defineComponent({
+    name: 'ProductDrawer',
+    components: { BasicDrawer, BasicForm, goodsSpecs, Input, InputNumber },
+    emits: ['reload', 'register'],
+    setup(_, { emit }) {
+      const isUpdate = ref(true);
+      const goodsRef = ref();
+      const { createMessage } = useMessage();
+      const userStore = useUserStore();
+      const userinfo = computed(() => userStore.getUserInfo);
+      const { t } = useI18n();
+
+      let editData = ref<EditDataType>({
+        categoryName: '',
+        productList: [],
+        id: 0,
+      });
+
+      const [registerForm, { resetFields, updateSchema, setFieldsValue, validate }] = useForm({
+        labelWidth: 120,
+        schemas: formSchema,
+        showActionButtonGroup: false,
+        baseColProps: { lg: 18, md: 18, offset: 1 },
+      });
+      const [itemRegister, {}] = useForm({
+        // formItemValue :validate appendSchemaByField, removeSchemaByFiled,
+        schemas: [
+          {
+            field: 'goodsId',
+            label: '规格名',
+            component: 'ApiTreeSelect',
+            componentProps: {
+              api: async (params) => {
+                const res = (await categoryApi(params)) as any as TreeNode[];
+                const treeData = makeTree(res);
+                return treeData;
+              },
+              maxLength: 15,
+              fieldNames: {
+                label: 'name',
+                key: 'id',
+                value: 'id',
+              },
+            },
+          },
+          {
+            field: 'goodsSn',
+            component: 'Input',
+            label: '字段0',
+            colProps: {
+              span: 8,
+            },
+            required: true,
+          },
+          {
+            field: '0',
+            component: 'Input',
+            label: ' ',
+            colProps: {
+              span: 8,
+            },
+            slot: 'add',
+          },
+        ],
+        labelWidth: 100,
+        actionColOptions: { span: 24 },
+      });
+      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+        resetFields();
+        setDrawerProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+        editData.value = {
+          categoryName: '',
+          productList: [],
+          id: 0,
+        };
+        updateSchema({
+          field: 'brandId',
+          componentProps: {
+            disabled: unref(isUpdate) ? true : false,
+          },
+        });
+        if (unref(isUpdate)) {
+          let res = await InfoApi(data.record.id);
+          editData.value = {
+            ...res,
+            productList: res.productList.map((ele) => {
+              console.log('editData', ele, res.goodsSpecificationList);
+              ele.name = res.goodsSpecificationList.find(
+                (element) => element.uuid == ele.uuidLink,
+              )?.specificationName;
+              return ele;
+            }),
+          };
+          setFieldsValue({
+            ...res,
+            isOnSale: res.isOnSale === 1,
+            listPicUrl:
+              res.listPicUrl && res.listPicUrl.split('#$#').map((ele) => ele.split('?')[0]),
+            primaryPicUrl:
+              res.primaryPicUrl && res.primaryPicUrl.split('#$#').map((ele) => ele.split('?')[0]),
+            // primaryPicUrl: res.primaryPicUrl && [encodeURI(res.primaryPicUrl.split('?')[0])],
+            steamRoom: data.record?.steamRoom?.name,
+          });
+        }
+      });
+      function setEditData(value) {
+        let productList = editData.value.productList || [];
+        value.productList.map((ele) => {
+          if (!productList.some((element) => ele.uuidLink == element.uuidLink)) {
+            //判断新增
+            productList.push({
+              uuidLink: ele.uuidLink,
+              name: ele.name,
+              goodsNumber: '',
+              goodsSn: '',
+              marketPrice: '',
+              retailPrice: '',
+            });
+          } else {
+            //本来就有的 修改赋值
+            productList.forEach((setEle) => {
+              if (ele.uuidLink == setEle.uuidLink) {
+                setEle.name = ele.name;
+              }
+            });
+          }
+        });
+        productList = productList.filter((ele) =>
+          value.productList.some((element) => element.uuidLink == ele.uuidLink),
+        );
+        console.log('setEditData', productList);
+        editData.value = {
+          ...editData.value,
+          ...value,
+          productList,
+        };
+        setFieldsValue({
+          productList: productList,
+        });
+      }
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增商品' : '编辑商品'));
+
+      async function handleSubmit() {
+        const { companyId, id } = userinfo.value;
+        try {
+          const values = await validate();
+          console.log('values', values);
+          let apiData = {
+            ...editData.value,
+            ...values,
+            retailPrice:
+              editData.value.productList &&
+              Math.min(...editData.value.productList.map((ele) => Number(ele.retailPrice))),
+            marketPrice:
+              editData.value.productList &&
+              Math.min(...editData.value.productList.map((ele) => Number(ele.marketPrice))),
+            isOnSale: values.isOnSale ? 1 : 0,
+          };
+
+          let requresApi = isUpdate.value ? UpdateSaleApi : SaveSaleApi;
+          if (!isUpdate.value) {
+            apiData.createUserDeptId = companyId;
+            apiData.createUserId = id;
+          }
+          let subData = await requresApi({
+            ...apiData,
+            primaryPicUrl: apiData.primaryPicUrl.join('#$#'),
+            listPicUrl: apiData.listPicUrl.join('#$#'),
+          });
+          console.log('setFieldsValue', subData);
+          setDrawerProps({ confirmLoading: true });
+          // TODO custom api
+          console.log(values);
+          createMessage.success(t('common.optSuccess'));
+          closeDrawer();
+          emit('reload');
+        } finally {
+          setDrawerProps({ confirmLoading: false });
+        }
+      }
+
+      return {
+        goodsRef,
+        registerDrawer,
+        registerForm,
+        itemRegister,
+        getTitle,
+        editData,
+        handleSubmit,
+        setEditData,
+      };
+    },
+  });
+</script>
+
+<style scoped lang="less">
+  :deep(.detailed) {
+    .title {
+      display: flex;
+      span,
+      input,
+      div {
+        flex-grow: 1;
+      }
+      input {
+        display: inline-block;
+      }
+      .name {
+        flex-grow: 2;
+      }
+    }
+    tr {
+      th {
+        font-weight: 500;
+      }
+    }
+  }
+</style>

+ 225 - 224
src/views/product/ref.vue

@@ -1,224 +1,225 @@
-<template>
-  <div>
-    <BasicTable @register="registerTable">
-      <template #toolbar>
-        <a-button v-show="!getCheckRole('tourist')" type="primary" @click="handleCreate">
-          新增商品属性</a-button
-        >
-      </template>
-      <template #action="{ record, column }">
-        <TableAction :actions="createActions(record, column)" />
-      </template>
-    </BasicTable>
-    <addModal @register="registerModal" @saveadd="handleSaveAdd" />
-  </div>
-</template>
-
-<script lang="ts">
-  import { defineComponent, ref } from 'vue'; // h
-  import {
-    BasicTable,
-    useTable,
-    BasicColumn,
-    TableAction,
-    EditRecordRow,
-    ActionItem,
-  } from '/@/components/Table';
-  import { attributeListApi, attributeDeleteApi, updateItemApi } from '/@/api/product/category';
-  import { cloneDeep } from 'lodash-es';
-  // import { Tag } from 'ant-design-vue';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import { useModal } from '/@/components/Modal';
-  import addModal from './addModal.vue';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { useUserStore } from '/@/store/modules/user';
-
-  export default defineComponent({
-    components: { BasicTable, TableAction, addModal },
-    setup() {
-      const { createConfirm, createMessage: msg } = useMessage();
-      const [registerModal, { openModal }] = useModal();
-      const currentEditKeyRef = ref('');
-      const userStore = useUserStore();
-      const { getCheckRole } = userStore;
-      const { t } = useI18n();
-      console.log('registerModal', registerModal);
-      const columns: BasicColumn[] = [
-        {
-          title: '商品属性',
-          editRow: true,
-          editRule: async (text) => {
-            if (!text) {
-              return '请输入商品属性';
-            }
-            if (text && text.length > 15) {
-              return t('routes.corporation.maxlength');
-            }
-            return '';
-          },
-          dataIndex: 'name',
-          fixed: 'left',
-          width: 100,
-        },
-        {
-          title: '排序',
-          dataIndex: 'sortOrder',
-          editComponent: 'InputNumber',
-          editRule: async (text) => {
-            if (text == null || text < 0) {
-              return '请输入正确排序';
-            }
-            return '';
-          },
-          editRow: true,
-          width: 50,
-        },
-      ];
-
-      const [registerTable, { expandAll, reload, collapseAll }] = useTable({
-        title: '商品属性',
-        api: attributeListApi,
-        columns: columns,
-        useSearchForm: true,
-        showIndexColumn: false,
-        // rowSelection: { type: 'checkbox' },
-        rowKey: 'id',
-        bordered: true,
-        showIndexColumn: true,
-        fetchSetting: {
-          pageField: 'page',
-          sizeField: 'limit',
-          listField: 'list',
-          totalField: 'totalCount',
-        },
-        formConfig: {
-          labelWidth: 66,
-          schemas: [
-            {
-              field: `name`,
-              label: `商品属性`,
-              component: 'Input',
-              colProps: {
-                xl: 6,
-                xxl: 6,
-              },
-              componentProps: {
-                maxLength: 15,
-              },
-            },
-          ],
-        },
-        actionColumn: {
-          ifShow: !getCheckRole('tourist'),
-          width: 160,
-          title: '操作',
-          dataIndex: 'action',
-          slots: { customRender: 'action' },
-        },
-      });
-      async function handleCreate() {
-        openModal(true);
-      }
-      function handleEdit(record: EditRecordRow) {
-        currentEditKeyRef.value = record.key;
-        record.onEdit?.(true);
-      }
-      function handleDelete(record) {
-        createConfirm({
-          iconType: 'warning',
-          title: '警告',
-          content: `此操作将对${record.name}进行删除, 是否继续?`,
-          onOk: async () => {
-            let res = await attributeDeleteApi(record.id);
-            console.log('res', res);
-            reload();
-          },
-        });
-      }
-      function handleCancel(record: EditRecordRow) {
-        currentEditKeyRef.value = '';
-        record.onEdit?.(false, false);
-      }
-      async function handleSaveAdd() {
-        console.log('handleSaveAdd');
-        reload();
-      }
-
-      async function handleSave(record: EditRecordRow) {
-        // 校验
-        msg.loading({ content: '正在保存...', duration: 0, key: 'saving' });
-        const valid = await record.onValid?.();
-        console.log('ref', valid);
-        if (valid) {
-          try {
-            const data = cloneDeep(record.editValueRefs);
-            console.log(data);
-            //TODO 此处将数据提交给服务器保存
-            // ...
-            updateItemApi({
-              id: record.id,
-              ...data,
-            });
-
-            // 保存之后提交编辑状态
-            const pass = await record.onEdit?.(false, true);
-            if (pass) {
-              currentEditKeyRef.value = '';
-            }
-            msg.success({ content: '数据已保存', key: 'saving' });
-            reload();
-          } catch (error) {
-            msg.error({ content: '保存失败', key: 'saving' });
-          }
-        } else {
-          msg.error({ content: '请填写正确的数据', key: 'saving' });
-        }
-      }
-      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
-        // console.log('editable', record);
-        if (!record.editable) {
-          return [
-            {
-              label: '编辑',
-              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              label: '删除',
-              color: 'error',
-              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
-              onClick: handleDelete.bind(null, record),
-            },
-          ];
-        }
-        return [
-          {
-            label: '保存',
-            onClick: handleSave.bind(null, record, column),
-          },
-          {
-            label: '取消',
-            popConfirm: {
-              title: '是否取消编辑',
-              confirm: handleCancel.bind(null, record, column),
-            },
-          },
-        ];
-      }
-      return {
-        registerTable,
-        createActions,
-        t,
-        registerModal,
-        handleSaveAdd,
-        handleCreate,
-        handleEdit,
-        handleDelete,
-        expandAll,
-        collapseAll,
-        getCheckRole,
-        reload,
-      };
-    },
-  });
-</script>
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button v-show="!getCheckRole('tourist')" type="primary" @click="handleCreate">
+          新增商品属性</a-button
+        >
+      </template>
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+    </BasicTable>
+    <addModal @register="registerModal" @saveadd="handleSaveAdd" />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue'; // h
+  import {
+    BasicTable,
+    useTable,
+    BasicColumn,
+    TableAction,
+    EditRecordRow,
+    ActionItem,
+  } from '/@/components/Table';
+  import { attributeListApi, attributeDeleteApi, updateItemApi } from '/@/api/product/category';
+  import { cloneDeep } from 'lodash-es';
+  // import { Tag } from 'ant-design-vue';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useModal } from '/@/components/Modal';
+  import addModal from './addModal.vue';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useUserStore } from '/@/store/modules/user';
+
+  export default defineComponent({
+    components: { BasicTable, TableAction, addModal },
+    emits: ['register'],
+    setup() {
+      const { createConfirm, createMessage: msg } = useMessage();
+      const [registerModal, { openModal }] = useModal();
+      const currentEditKeyRef = ref('');
+      const userStore = useUserStore();
+      const { getCheckRole } = userStore;
+      const { t } = useI18n();
+      // console.log('registerModal', registerModal);
+      const columns: BasicColumn[] = [
+        {
+          title: '商品属性',
+          editRow: true,
+          editRule: async (text) => {
+            if (!text) {
+              return '请输入商品属性';
+            }
+            if (text && text.length > 15) {
+              return t('routes.corporation.maxlength');
+            }
+            return '';
+          },
+          dataIndex: 'name',
+          fixed: 'left',
+          width: 100,
+        },
+        {
+          title: '排序',
+          dataIndex: 'sortOrder',
+          editComponent: 'InputNumber',
+          editRule: async (text) => {
+            if (text == null || text < 0) {
+              return '请输入正确排序';
+            }
+            return '';
+          },
+          editRow: true,
+          width: 50,
+        },
+      ];
+
+      const [registerTable, { expandAll, reload, collapseAll }] = useTable({
+        title: '商品属性',
+        api: attributeListApi,
+        columns: columns,
+        useSearchForm: true,
+        showIndexColumn: false,
+        // rowSelection: { type: 'checkbox' },
+        // rowKey: 'id',
+        bordered: true,
+        fetchSetting: {
+          pageField: 'page',
+          sizeField: 'limit',
+          listField: 'list',
+          totalField: 'totalCount',
+        },
+        formConfig: {
+          labelWidth: 66,
+          schemas: [
+            {
+              field: `name`,
+              label: `商品属性`,
+              component: 'Input',
+              colProps: {
+                xl: 6,
+                xxl: 6,
+              },
+              componentProps: {
+                maxLength: 15,
+              },
+            },
+          ],
+        },
+        actionColumn: {
+          ifShow: !getCheckRole('tourist'),
+          width: 160,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+        },
+      });
+      async function handleCreate() {
+        openModal(true);
+      }
+      function handleEdit(record: EditRecordRow) {
+        currentEditKeyRef.value = record.key;
+        record.onEdit?.(true);
+      }
+      function handleDelete(record) {
+        createConfirm({
+          iconType: 'warning',
+          title: '警告',
+          content: `此操作将对${record.name}进行删除, 是否继续?`,
+          onOk: async () => {
+            let res = await attributeDeleteApi(record.id);
+            console.log('res', res);
+            reload();
+          },
+        });
+      }
+      function handleCancel(record: EditRecordRow) {
+        currentEditKeyRef.value = '';
+        record.onEdit?.(false, false);
+        reload();
+      }
+      async function handleSaveAdd() {
+        console.log('handleSaveAdd');
+        reload();
+      }
+
+      async function handleSave(record: EditRecordRow) {
+        // 校验
+        msg.loading({ content: '正在保存...', duration: 0, key: 'saving' });
+        const valid = await record.onValid?.();
+        console.log('ref', valid);
+        if (valid) {
+          try {
+            const data = cloneDeep(record.editValueRefs);
+            console.log(data);
+            //TODO 此处将数据提交给服务器保存
+            // ...
+            updateItemApi({
+              id: record.id,
+              ...data,
+            });
+
+            // 保存之后提交编辑状态
+            const pass = await record.onEdit?.(false, true);
+            if (pass) {
+              currentEditKeyRef.value = '';
+            }
+            msg.success({ content: '数据已保存', key: 'saving' });
+            reload();
+          } catch (error) {
+            msg.error({ content: '保存失败', key: 'saving' });
+          }
+        } else {
+          msg.error({ content: '请填写正确的数据', key: 'saving' });
+        }
+      }
+      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
+        // console.log('editable', record);
+        if (!record.editable) {
+          return [
+            {
+              label: '编辑',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              label: '删除',
+              color: 'error',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleDelete.bind(null, record),
+            },
+          ];
+        }
+        return [
+          {
+            label: '保存',
+            onClick: handleSave.bind(null, record, column),
+          },
+          {
+            label: '取消',
+            popConfirm: {
+              title: '是否取消编辑',
+              confirm: handleCancel.bind(null, record, column),
+            },
+          },
+        ];
+      }
+      return {
+        registerTable,
+        createActions,
+        t,
+        registerModal,
+        handleSaveAdd,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        expandAll,
+        collapseAll,
+        getCheckRole,
+        reload,
+      };
+    },
+  });
+</script>

+ 1 - 1
src/views/rightsEnterprises/BindModal.vue

@@ -37,7 +37,7 @@
     props: {
       userData: { type: Object },
     },
-    emits: ['ok'],
+    emits: ['ok', 'register'],
     setup(_, context) {
       const modelRef = ref({
         isSee: false,

+ 3 - 1
src/views/rightsEnterprises/addCameraModal.vue

@@ -31,7 +31,7 @@
     props: {
       userData: { type: Object },
     },
-    emits: ['ok'],
+    emits: ['ok', 'register'],
     setup(_, context) {
       const modelRef = ref({
         isSee: false,
@@ -58,6 +58,7 @@
             resultField: 'list',
             labelField: 'name',
             valueField: 'id',
+            optionFilterProp: 'label',
             immediate: true,
             params: {
               page: 1,
@@ -89,6 +90,7 @@
             resultField: 'list',
             labelField: 'name',
             valueField: 'id',
+            optionFilterProp: 'label',
             immediate: true,
             params: {
               page: 1,

+ 0 - 0
src/views/rightsEnterprises/camera.vue


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott