xyh 2 лет назад
Родитель
Сommit
face79c241
48 измененных файлов с 1243 добавлено и 30 удалено
  1. 1 0
      .eslintrc.js
  2. 6 1
      package.json
  3. 128 7
      pnpm-lock.yaml
  4. 9 4
      src/App.vue
  5. 0 5
      src/apis/.gitkeep
  6. 2 0
      src/apis/demo.ts
  7. 0 0
      src/apis/index.ts
  8. 61 0
      src/apis/network.ts
  9. 1 0
      src/assets/images/modal/add.svg
  10. 1 0
      src/assets/images/modal/edit.svg
  11. 1 0
      src/assets/images/modal/filter.svg
  12. 1 0
      src/components/button/Button.tsx
  13. 3 0
      src/components/filter/button-group/index.css
  14. 59 0
      src/components/filter/button-group/index.tsx
  15. 69 0
      src/components/filter/date/index.tsx
  16. 164 0
      src/components/filter/group/index.vue
  17. 24 0
      src/components/filter/group/state.ts
  18. 11 0
      src/components/filter/hooks.ts
  19. 30 0
      src/components/filter/index.css
  20. 6 0
      src/components/filter/index.ts
  21. 42 0
      src/components/filter/input/index.tsx
  22. 88 0
      src/components/filter/select/index.tsx
  23. 12 0
      src/components/filter/wrapper/index.css
  24. 42 0
      src/components/filter/wrapper/index.tsx
  25. 2 0
      src/components/index.ts
  26. 1 0
      src/components/modal/index.ts
  27. 133 0
      src/components/modal/normal/index.css
  28. 80 0
      src/components/modal/normal/index.tsx
  29. 9 1
      src/locales/common.ts
  30. 1 0
      src/main.ts
  31. 0 5
      src/models/.gitkeep
  32. 2 0
      src/models/index.ts
  33. 10 0
      src/models/request/index.ts
  34. 30 0
      src/models/response/index.ts
  35. 36 0
      src/pages/audit/filter/hooks.ts
  36. 69 0
      src/pages/audit/filter/index.vue
  37. 11 0
      src/pages/audit/index.vue
  38. 3 0
      src/pages/audit/state.ts
  39. 15 0
      src/pages/home/index.css
  40. 7 1
      src/pages/home/index.vue
  41. 1 0
      src/reset.d.ts
  42. 21 2
      src/routes/index.ts
  43. 7 0
      src/routes/name.ts
  44. 18 1
      src/styles/variable.css
  45. 5 0
      src/utils/constants.ts
  46. 1 0
      src/utils/index.ts
  47. 20 2
      src/utils/theme.ts
  48. 0 1
      vite.config.ts

+ 1 - 0
.eslintrc.js

@@ -26,6 +26,7 @@ module.exports = {
     extraFileExtensions: ['.vue'],
     extraFileExtensions: ['.vue'],
   },
   },
   rules: {
   rules: {
+    'vue/require-default-prop': 0,
     curly: [1, 'multi-or-nest', 'consistent'],
     curly: [1, 'multi-or-nest', 'consistent'],
     '@typescript-eslint/indent': 0,
     '@typescript-eslint/indent': 0,
     indent: [
     indent: [

+ 6 - 1
package.json

@@ -78,6 +78,7 @@
     "@icon-park/vue-next": "^1.4.2",
     "@icon-park/vue-next": "^1.4.2",
     "@imengyu/vue3-context-menu": "^1.2.6",
     "@imengyu/vue3-context-menu": "^1.2.6",
     "@tanstack/vue-query": "^4.29.7",
     "@tanstack/vue-query": "^4.29.7",
+    "@tanstack/vue-table": "^8.9.1",
     "@vee-validate/zod": "^4.9.3",
     "@vee-validate/zod": "^4.9.3",
     "@vueuse/core": "^10.1.2",
     "@vueuse/core": "^10.1.2",
     "@vueuse/integrations": "^10.1.2",
     "@vueuse/integrations": "^10.1.2",
@@ -87,18 +88,22 @@
     "lodash-es": "^4.17.21",
     "lodash-es": "^4.17.21",
     "naive-ui": "^2.34.3",
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.36",
     "pinia": "^2.0.36",
+    "react-dnd-html5-backend": "^16.0.1",
     "sortablejs": "^1.15.0",
     "sortablejs": "^1.15.0",
     "veboundary": "1.2.2",
     "veboundary": "1.2.2",
     "vee-validate": "^4.9.3",
     "vee-validate": "^4.9.3",
     "vue": "^3.3.2",
     "vue": "^3.3.2",
+    "vue-final-modal": "^4.4.2",
     "vue-i18n": "^9.2.2",
     "vue-i18n": "^9.2.2",
     "vue-router": "^4.2.0",
     "vue-router": "^4.2.0",
+    "vue3-dnd": "^2.0.2",
     "zod": "^3.21.4"
     "zod": "^3.21.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@commitlint/cli": "^17.6.3",
     "@commitlint/cli": "^17.6.3",
     "@commitlint/config-conventional": "^17.6.3",
     "@commitlint/config-conventional": "^17.6.3",
     "@nabla/vite-plugin-eslint": "^1.5.0",
     "@nabla/vite-plugin-eslint": "^1.5.0",
+    "@total-typescript/ts-reset": "^0.4.2",
     "@types/lodash-es": "^4.17.7",
     "@types/lodash-es": "^4.17.7",
     "@types/node": "20.1.4",
     "@types/node": "20.1.4",
     "@types/rollup-plugin-visualizer": "^4.2.1",
     "@types/rollup-plugin-visualizer": "^4.2.1",
@@ -116,7 +121,7 @@
     "editorconfig": "^1.0.2",
     "editorconfig": "^1.0.2",
     "eslint": "^8.40.0",
     "eslint": "^8.40.0",
     "eslint-config-prettier": "^8.8.0",
     "eslint-config-prettier": "^8.8.0",
-    "eslint-config-proste": "^7.1.0",
+    "eslint-config-proste": "^7.4.0",
     "eslint-plugin-cypress": "^2.13.3",
     "eslint-plugin-cypress": "^2.13.3",
     "eslint-plugin-import": "^2.27.5",
     "eslint-plugin-import": "^2.27.5",
     "eslint-plugin-import-newlines": "^1.3.1",
     "eslint-plugin-import-newlines": "^1.3.1",

+ 128 - 7
pnpm-lock.yaml

@@ -10,6 +10,9 @@ dependencies:
   '@tanstack/vue-query':
   '@tanstack/vue-query':
     specifier: ^4.29.7
     specifier: ^4.29.7
     version: 4.29.7(vue@3.3.2)
     version: 4.29.7(vue@3.3.2)
+  '@tanstack/vue-table':
+    specifier: ^8.9.1
+    version: 8.9.1(vue@3.3.2)
   '@vee-validate/zod':
   '@vee-validate/zod':
     specifier: ^4.9.3
     specifier: ^4.9.3
     version: 4.9.3(typescript@5.0.4)(vue@3.3.2)
     version: 4.9.3(typescript@5.0.4)(vue@3.3.2)
@@ -18,7 +21,7 @@ dependencies:
     version: 10.1.2(vue@3.3.2)
     version: 10.1.2(vue@3.3.2)
   '@vueuse/integrations':
   '@vueuse/integrations':
     specifier: ^10.1.2
     specifier: ^10.1.2
-    version: 10.1.2(axios@1.4.0)(sortablejs@1.15.0)(vue@3.3.2)
+    version: 10.1.2(axios@1.4.0)(focus-trap@7.4.3)(sortablejs@1.15.0)(vue@3.3.2)
   axios:
   axios:
     specifier: ^1.4.0
     specifier: ^1.4.0
     version: 1.4.0
     version: 1.4.0
@@ -37,6 +40,9 @@ dependencies:
   pinia:
   pinia:
     specifier: ^2.0.36
     specifier: ^2.0.36
     version: 2.0.36(typescript@5.0.4)(vue@3.3.2)
     version: 2.0.36(typescript@5.0.4)(vue@3.3.2)
+  react-dnd-html5-backend:
+    specifier: ^16.0.1
+    version: 16.0.1
   sortablejs:
   sortablejs:
     specifier: ^1.15.0
     specifier: ^1.15.0
     version: 1.15.0
     version: 1.15.0
@@ -49,12 +55,18 @@ dependencies:
   vue:
   vue:
     specifier: ^3.3.2
     specifier: ^3.3.2
     version: 3.3.2
     version: 3.3.2
+  vue-final-modal:
+    specifier: ^4.4.2
+    version: 4.4.2(@vueuse/core@10.1.2)(@vueuse/integrations@10.1.2)(focus-trap@7.4.3)(vue@3.3.2)
   vue-i18n:
   vue-i18n:
     specifier: ^9.2.2
     specifier: ^9.2.2
     version: 9.2.2(vue@3.3.2)
     version: 9.2.2(vue@3.3.2)
   vue-router:
   vue-router:
     specifier: ^4.2.0
     specifier: ^4.2.0
     version: 4.2.0(vue@3.3.2)
     version: 4.2.0(vue@3.3.2)
+  vue3-dnd:
+    specifier: ^2.0.2
+    version: 2.0.2(vue@3.3.2)
   zod:
   zod:
     specifier: ^3.21.4
     specifier: ^3.21.4
     version: 3.21.4
     version: 3.21.4
@@ -69,6 +81,9 @@ devDependencies:
   '@nabla/vite-plugin-eslint':
   '@nabla/vite-plugin-eslint':
     specifier: ^1.5.0
     specifier: ^1.5.0
     version: 1.5.0(eslint@8.40.0)(vite@4.3.5)
     version: 1.5.0(eslint@8.40.0)(vite@4.3.5)
+  '@total-typescript/ts-reset':
+    specifier: ^0.4.2
+    version: 0.4.2
   '@types/lodash-es':
   '@types/lodash-es':
     specifier: ^4.17.7
     specifier: ^4.17.7
     version: 4.17.7
     version: 4.17.7
@@ -121,8 +136,8 @@ devDependencies:
     specifier: ^8.8.0
     specifier: ^8.8.0
     version: 8.8.0(eslint@8.40.0)
     version: 8.8.0(eslint@8.40.0)
   eslint-config-proste:
   eslint-config-proste:
-    specifier: ^7.1.0
-    version: 7.1.0(@typescript-eslint/eslint-plugin@5.59.5)(@typescript-eslint/parser@5.59.5)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-vue@9.13.0)(eslint@8.40.0)
+    specifier: ^7.4.0
+    version: 7.4.0(@typescript-eslint/eslint-plugin@5.59.5)(@typescript-eslint/parser@5.59.5)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-vue@9.13.0)(eslint@8.40.0)
   eslint-plugin-cypress:
   eslint-plugin-cypress:
     specifier: ^2.13.3
     specifier: ^2.13.3
     version: 2.13.3(eslint@8.40.0)
     version: 2.13.3(eslint@8.40.0)
@@ -1449,6 +1464,26 @@ packages:
     resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
     resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
     dev: true
     dev: true
 
 
+  /@react-dnd/asap@4.0.1:
+    resolution: {integrity: sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==}
+    dev: false
+
+  /@react-dnd/asap@5.0.2:
+    resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
+    dev: false
+
+  /@react-dnd/invariant@3.0.1:
+    resolution: {integrity: sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g==}
+    dev: false
+
+  /@react-dnd/invariant@4.0.2:
+    resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
+    dev: false
+
+  /@react-dnd/shallowequal@3.0.1:
+    resolution: {integrity: sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA==}
+    dev: false
+
   /@tanstack/match-sorter-utils@8.8.4:
   /@tanstack/match-sorter-utils@8.8.4:
     resolution: {integrity: sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==}
     resolution: {integrity: sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -1460,6 +1495,11 @@ packages:
     resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
     resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
     dev: false
     dev: false
 
 
+  /@tanstack/table-core@8.9.1:
+    resolution: {integrity: sha512-2+R83n8vMZND0q3W1lSiF7co9nFbeWbjAErFf27xwbeA9E0wtUu5ZDfgj+TZ6JzdAEQAgfxkk/QNFAKiS8E4MA==}
+    engines: {node: '>=12'}
+    dev: false
+
   /@tanstack/vue-query@4.29.7(vue@3.3.2):
   /@tanstack/vue-query@4.29.7(vue@3.3.2):
     resolution: {integrity: sha512-dVk2s5zcEtkDiNV4EFvKRiTnldNCcWh4P57QmxuBNZrJ2bncumVoWO1PAO8uPfucHV0qZtFWDyzXzHyrdkHK7g==}
     resolution: {integrity: sha512-dVk2s5zcEtkDiNV4EFvKRiTnldNCcWh4P57QmxuBNZrJ2bncumVoWO1PAO8uPfucHV0qZtFWDyzXzHyrdkHK7g==}
     peerDependencies:
     peerDependencies:
@@ -1476,11 +1516,25 @@ packages:
       vue-demi: 0.13.11(vue@3.3.2)
       vue-demi: 0.13.11(vue@3.3.2)
     dev: false
     dev: false
 
 
+  /@tanstack/vue-table@8.9.1(vue@3.3.2):
+    resolution: {integrity: sha512-Z5MEuPGedmQShV/Md3ABm8Ju5k4NtGtAgB2C+lhuF3/geQl+OZelbtQ1j3l5jnIIgyBk++BkjQJPq4EaEN5uOg==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      vue: ^3.2.33
+    dependencies:
+      '@tanstack/table-core': 8.9.1
+      vue: 3.3.2
+    dev: false
+
   /@tootallnate/once@2.0.0:
   /@tootallnate/once@2.0.0:
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     dev: true
     dev: true
 
 
+  /@total-typescript/ts-reset@0.4.2:
+    resolution: {integrity: sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==}
+    dev: true
+
   /@tsconfig/node10@1.0.9:
   /@tsconfig/node10@1.0.9:
     resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
     resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
     dev: true
     dev: true
@@ -2002,7 +2056,7 @@ packages:
       - vue
       - vue
     dev: false
     dev: false
 
 
-  /@vueuse/integrations@10.1.2(axios@1.4.0)(sortablejs@1.15.0)(vue@3.3.2):
+  /@vueuse/integrations@10.1.2(axios@1.4.0)(focus-trap@7.4.3)(sortablejs@1.15.0)(vue@3.3.2):
     resolution: {integrity: sha512-wUpG3Wv6LiWerOwCzOAM0iGhNQ4vfFUTkhj/xQy7TLXduh2M3D8N08aS0KqlxsejY6R8NLxydDIM+68QfHZZ8Q==}
     resolution: {integrity: sha512-wUpG3Wv6LiWerOwCzOAM0iGhNQ4vfFUTkhj/xQy7TLXduh2M3D8N08aS0KqlxsejY6R8NLxydDIM+68QfHZZ8Q==}
     peerDependencies:
     peerDependencies:
       async-validator: '*'
       async-validator: '*'
@@ -2046,6 +2100,7 @@ packages:
       '@vueuse/core': 10.1.2(vue@3.3.2)
       '@vueuse/core': 10.1.2(vue@3.3.2)
       '@vueuse/shared': 10.1.2(vue@3.3.2)
       '@vueuse/shared': 10.1.2(vue@3.3.2)
       axios: 1.4.0
       axios: 1.4.0
+      focus-trap: 7.4.3
       sortablejs: 1.15.0
       sortablejs: 1.15.0
       vue-demi: 0.14.1(vue@3.3.2)
       vue-demi: 0.14.1(vue@3.3.2)
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -3067,6 +3122,22 @@ packages:
       path-type: 4.0.0
       path-type: 4.0.0
     dev: true
     dev: true
 
 
+  /dnd-core@15.1.2:
+    resolution: {integrity: sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A==}
+    dependencies:
+      '@react-dnd/asap': 4.0.1
+      '@react-dnd/invariant': 3.0.1
+      redux: 4.2.1
+    dev: false
+
+  /dnd-core@16.0.1:
+    resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
+    dependencies:
+      '@react-dnd/asap': 5.0.2
+      '@react-dnd/invariant': 4.0.2
+      redux: 4.2.1
+    dev: false
+
   /doctrine@2.1.0:
   /doctrine@2.1.0:
     resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
     resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
@@ -3281,8 +3352,8 @@ packages:
       eslint: 8.40.0
       eslint: 8.40.0
     dev: true
     dev: true
 
 
-  /eslint-config-proste@7.1.0(@typescript-eslint/eslint-plugin@5.59.5)(@typescript-eslint/parser@5.59.5)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-vue@9.13.0)(eslint@8.40.0):
-    resolution: {integrity: sha512-i+w0HCOZxyN936cRHgEN7PV/218RP9YocJzMWTPizuU35jrvXemZ5yI7qSFZRGF1SGnzjObIW5u/ZV2YFxAFPA==}
+  /eslint-config-proste@7.4.0(@typescript-eslint/eslint-plugin@5.59.5)(@typescript-eslint/parser@5.59.5)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-vue@9.13.0)(eslint@8.40.0):
+    resolution: {integrity: sha512-DJckpPELQCwqoXN8ucan54NRbhg0ry2x5ELnUtAJYEX1NCj8HTqrBfOkE0+UZVSyNfANLFI7+jUx2/hBXGU8+A==}
     peerDependencies:
     peerDependencies:
       '@typescript-eslint/eslint-plugin': '>=5.4.0'
       '@typescript-eslint/eslint-plugin': '>=5.4.0'
       '@typescript-eslint/parser': '>=5.4.0'
       '@typescript-eslint/parser': '>=5.4.0'
@@ -3633,7 +3704,6 @@ packages:
 
 
   /fast-deep-equal@3.1.3:
   /fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
-    dev: true
 
 
   /fast-diff@1.2.0:
   /fast-diff@1.2.0:
     resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
     resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
@@ -3745,6 +3815,12 @@ packages:
     resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
     resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
     dev: true
     dev: true
 
 
+  /focus-trap@7.4.3:
+    resolution: {integrity: sha512-BgSSbK4GPnS2VbtZ50VtOv1Sti6DIkj3+LkVjiWMNjLeAp1SH1UlLx3ULu/DCu4vq5R4/uvTm+zrvsMsuYmGLg==}
+    dependencies:
+      tabbable: 6.1.2
+    dev: false
+
   /follow-redirects@1.15.2:
   /follow-redirects@1.15.2:
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
     engines: {node: '>=4.0'}
@@ -5879,6 +5955,12 @@ packages:
     engines: {node: '>=8'}
     engines: {node: '>=8'}
     dev: true
     dev: true
 
 
+  /react-dnd-html5-backend@16.0.1:
+    resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
+    dependencies:
+      dnd-core: 16.0.1
+    dev: false
+
   /react-is@17.0.2:
   /react-is@17.0.2:
     resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
     resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
     dev: true
     dev: true
@@ -5919,6 +6001,12 @@ packages:
       strip-indent: 3.0.0
       strip-indent: 3.0.0
     dev: true
     dev: true
 
 
+  /redux@4.2.1:
+    resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
+    dependencies:
+      '@babel/runtime': 7.21.5
+    dev: false
+
   /regenerator-runtime@0.13.11:
   /regenerator-runtime@0.13.11:
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
     dev: false
     dev: false
@@ -6497,6 +6585,10 @@ packages:
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
     dev: true
     dev: true
 
 
+  /tabbable@6.1.2:
+    resolution: {integrity: sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==}
+    dev: false
+
   /table@6.8.1:
   /table@6.8.1:
     resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==}
     resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==}
     engines: {node: '>=10.0.0'}
     engines: {node: '>=10.0.0'}
@@ -7028,6 +7120,20 @@ packages:
       - supports-color
       - supports-color
     dev: true
     dev: true
 
 
+  /vue-final-modal@4.4.2(@vueuse/core@10.1.2)(@vueuse/integrations@10.1.2)(focus-trap@7.4.3)(vue@3.3.2):
+    resolution: {integrity: sha512-KO7I7cNgOI28MVS1JOelJrbZNSomPog0lMS8oeETo7XkH4BLpGyntbvasW63Zg0xpawuVwZ7oV3xb7qeHLSNxw==}
+    peerDependencies:
+      '@vueuse/core': '>=9.11.1'
+      '@vueuse/integrations': '>=9.11.1'
+      focus-trap: '>=7.2.0'
+      vue: '>=3.2.0'
+    dependencies:
+      '@vueuse/core': 10.1.2(vue@3.3.2)
+      '@vueuse/integrations': 10.1.2(axios@1.4.0)(focus-trap@7.4.3)(sortablejs@1.15.0)(vue@3.3.2)
+      focus-trap: 7.4.3
+      vue: 3.3.2
+    dev: false
+
   /vue-i18n@9.2.2(vue@3.3.2):
   /vue-i18n@9.2.2(vue@3.3.2):
     resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==}
     resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==}
     engines: {node: '>= 14'}
     engines: {node: '>= 14'}
@@ -7069,6 +7175,21 @@ packages:
       typescript: 5.0.4
       typescript: 5.0.4
     dev: true
     dev: true
 
 
+  /vue3-dnd@2.0.2(vue@3.3.2):
+    resolution: {integrity: sha512-DyS6D/LkM2vGFmQy8yhTNvjQfM7wjLO1aDirpM/WGmq3rMWEpzFNg+xrJj+BWxfNPJsmzkY3ZacbaWJUM1OKKg==}
+    peerDependencies:
+      vue: ^3.0.0-0 || ^2.6.0
+    dependencies:
+      '@react-dnd/invariant': 3.0.1
+      '@react-dnd/shallowequal': 3.0.1
+      dnd-core: 15.1.2
+      fast-deep-equal: 3.1.3
+      vue: 3.3.2
+      vue-demi: 0.13.11(vue@3.3.2)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+    dev: false
+
   /vue@3.3.2:
   /vue@3.3.2:
     resolution: {integrity: sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==}
     resolution: {integrity: sha512-98hJcAhyDwZoOo2flAQBSPVYG/o0HA9ivIy2ktHshjE+6/q8IMQ+kvDKQzOZTFPxvnNMcGM+zS2A00xeZMA7tA==}
     dependencies:
     dependencies:

+ 9 - 4
src/App.vue

@@ -20,9 +20,7 @@ defineOptions({
 const {locale} = useI18n();
 const {locale} = useI18n();
 
 
 const uiLocale = computed(function() {
 const uiLocale = computed(function() {
-  if (locale.value === 'zh') {
-    return {locale: zhCN, dateLocale: dateZhCN};
-  }
+  if (locale.value === 'zh') return {locale: zhCN, dateLocale: dateZhCN};
 
 
   return {locale: koKR, dateLocale: dateKoKR};
   return {locale: koKR, dateLocale: dateKoKR};
 });
 });
@@ -31,8 +29,12 @@ const themeConfig: GlobalThemeOverrides = {
   common: {
   common: {
     primaryColor: lightVariable.primaryColor,
     primaryColor: lightVariable.primaryColor,
     primaryColorHover: lightVariable.primaryColor4,
     primaryColorHover: lightVariable.primaryColor4,
-    primaryColorPressed: lightVariable.primaryColor2,
+    primaryColorPressed: lightVariable.primaryColor,
     primaryColorSuppl: lightVariable.primaryColor,
     primaryColorSuppl: lightVariable.primaryColor,
+    infoColor: lightVariable.accentColor,
+    infoColorHover: lightVariable.accentColor6,
+    infoColorPressed: lightVariable.accentColor,
+    infoColorSuppl: lightVariable.accentColor,
   },
   },
   Scrollbar: {
   Scrollbar: {
     color: lightVariable.primaryColor8,
     color: lightVariable.primaryColor8,
@@ -52,6 +54,9 @@ const themeConfig: GlobalThemeOverrides = {
     siderColor: lightVariable.layoutBackgroundColor,
     siderColor: lightVariable.layoutBackgroundColor,
     siderBorderColor: lightVariable.backgroundColor,
     siderBorderColor: lightVariable.backgroundColor,
   },
   },
+  Card: {
+    borderRadius: '8px',
+  },
 };
 };
 
 
 </script>
 </script>

+ 0 - 5
src/apis/.gitkeep

@@ -1,5 +0,0 @@
-# To ensure that empty folders can also be submitted
-
-# You can delete this file
-
-# This type of file has been entered in .gitignore, and this type of file will not be submitted to git

+ 2 - 0
src/apis/demo.ts

@@ -0,0 +1,2 @@
+import {request} from './network';
+

+ 0 - 0
src/apis/index.ts


+ 61 - 0
src/apis/network.ts

@@ -0,0 +1,61 @@
+import {BaseResultContent} from '@models';
+import {NETWORK_URL} from '@utils';
+import axios from 'axios';
+import {useMessage} from 'naive-ui';
+
+// 普通请求
+const http = axios.create({
+  baseURL: NETWORK_URL,
+  headers: {
+    'Content-Type': 'application/json',
+    'Cache-Control': 'no-cache',
+  },
+});
+
+const exportReg = /export|excel/i;
+
+http.interceptors.request.use(function(config) {
+  if (config?.url) {
+    const isExport = exportReg.test(config.url);
+    isExport && (config.responseType = 'blob');
+  }
+
+  return config;
+});
+
+export async function request<T, R extends BaseResultContent<any>>(options: {
+  method: 'POST' | 'GET' | 'PUT' | 'DELETE';
+  url: string;
+  data?: T;
+  skipError?: boolean;
+  useBody?: boolean;
+  signal?: AbortSignal;
+}) {
+  const {data, skipError, method, url, useBody, signal} = options;
+
+  let res: BaseResultContent<any>;
+  const useData = useBody ? useBody : method !== 'GET' && method !== 'DELETE';
+  const message = useMessage();
+
+  try {
+    const result = await http.request<R>({
+      method,
+      url,
+      data: useData ? data : void 0,
+      params: !useData ? data : void 0,
+      signal,
+    });
+
+    res = result.data;
+
+    if (res.msg !== '200' && !skipError && !exportReg.test(url))
+      message.error(res.errMsg);
+  } catch (error: any) {
+    res = {msg: '510', errMsg: 'NETWORK_ERROR'};
+
+    if (!skipError && error.code !== 'ERR_CANCELED' && !exportReg.test(url))
+      message.error(res.errMsg);
+  }
+
+  return res;
+}

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/images/modal/add.svg


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/images/modal/edit.svg


+ 1 - 0
src/assets/images/modal/filter.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9L20.4 25.8178V38.4444L27.6 42V25.8178L42 9H6Z" fill="none" stroke="#ffffff" stroke-width="4" stroke-linejoin="round"/></svg>

+ 1 - 0
src/components/button/Button.tsx

@@ -17,6 +17,7 @@ export default defineComponent({
         class="ld-button"
         class="ld-button"
         {...props}
         {...props}
         type="primary"
         type="primary"
+        focusable={false}
       >
       >
         {slots.default?.()}
         {slots.default?.()}
       </NButton>
       </NButton>

+ 3 - 0
src/components/filter/button-group/index.css

@@ -0,0 +1,3 @@
+.ld-filter-button {
+  --n-border-radius: 6px !important;
+}

+ 59 - 0
src/components/filter/button-group/index.tsx

@@ -0,0 +1,59 @@
+import './index.css';
+import {PropType, defineComponent, h} from 'vue';
+import {NButton, NSpace} from 'naive-ui';
+import {useI18n} from 'vue-i18n';
+import {Search, Clear, Filter} from '@icon-park/vue-next';
+
+const defaultProps = {
+  class: 'ld-filter-button',
+  focusable: false,
+};
+
+export default defineComponent({
+  name: 'LDFilterButtonGroup',
+  props: {
+    onFilter: Function as PropType<() => void>,
+    isSearching: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  setup() {
+    const {t} = useI18n();
+
+    return {t};
+  },
+  render() {
+    const {t} = this;
+    const {onFilter, isSearching} = this.$props;
+
+    return (
+      <NSpace>
+        <NButton
+          type="primary"
+          {...defaultProps}
+          renderIcon={() => h(Search)}
+          attrType="submit"
+          loading={isSearching}
+        >
+          {t('common.filter.search')}
+        </NButton>
+        <NButton
+          {...defaultProps}
+          type="info"
+          renderIcon={() => h(Clear)}
+          attrType="reset"
+        >
+          {t('common.filter.reset')}
+        </NButton>
+        {onFilter && <NButton
+          {...defaultProps}
+          renderIcon={() => h(Filter)}
+          onClick={onFilter}
+        >
+          {t('common.filter.filter')}
+        </NButton>}
+      </NSpace>
+    );
+  },
+});

+ 69 - 0
src/components/filter/date/index.tsx

@@ -0,0 +1,69 @@
+import '../index.css';
+import {PropType, computed, defineComponent} from 'vue';
+import {NDatePicker} from 'naive-ui';
+import {useField} from '../hooks';
+
+export default defineComponent({
+  name: 'LDFilterDate',
+  props: {
+    providerKey: {
+      type: Symbol,
+      required: true,
+    },
+    name: {
+      type: Array as unknown as PropType<[string, string]>,
+      required: true,
+    },
+    label: {
+      type: String as PropType<string>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const [startTime, setStartTime]
+    = useField<number | null>(props.providerKey, props.name[0]);
+    const [endTime, setEndTime]
+    = useField<number | null>(props.providerKey, props.name[1]);
+
+    const value = computed<[number, number] | null>(function() {
+      if (startTime.value === null || endTime.value === null)
+        return null;
+
+      return [startTime.value, endTime.value];
+    });
+
+    function onClear() {
+      setStartTime(null);
+      setEndTime(null);
+    }
+
+    function onConfirm(value: [number, number]) {
+      setStartTime(value[0]);
+      setEndTime(value[1]);
+    }
+
+    return {
+      value,
+      onClear,
+      onConfirm,
+    };
+  },
+  render() {
+    const {label} = this.$props;
+    const {onClear, value, onConfirm} = this;
+
+    return (
+      <div class="ld-filter-field-wrapper">
+        <label>{label}</label>
+
+        <NDatePicker
+          type="daterange"
+          clearable
+          onClear={onClear}
+          onConfirm={onConfirm}
+          value={value}
+        />
+      </div>
+    );
+  },
+});

+ 164 - 0
src/components/filter/group/index.vue

@@ -0,0 +1,164 @@
+<script setup lang='ts' generic="T extends Record<string, unknown>">
+import type {LDFilterTool, LDFilterToolMap} from './state';
+import {
+  LDFilterWrapper,
+  LDFilterInput,
+  LDFilterSelect,
+  LDFilterDate,
+  LDNormalModal,
+} from '../..';
+import {ref, computed, watchEffect, watch, inject} from 'vue';
+import {NTransfer} from 'naive-ui';
+import {differenceBy} from 'lodash-es';
+
+defineOptions({name: 'LDFilterGroup'});
+
+type Props = {
+  providerKey: symbol,
+  fixedTools: LDFilterTool<T>[];
+  sourceMap?: LDFilterToolMap<T>;
+  sourceTools?: string;
+  isSearching: boolean;
+  onSubmit: (e?: Event) => void;
+  onReset: (e: Event) => void;
+  onFilterConfirm?: (value: string) => void;
+};
+
+const props = defineProps<Props>();
+
+// #region 筛选框内容
+const toolList = computed(function() {
+  if (!props.sourceTools || !props.sourceMap) return props.fixedTools;
+
+  const arr = props.sourceTools.split(',');
+
+  const els = arr.map(function(id) {
+    if (!props.sourceMap!.has(id))
+      return;
+
+    return {...props.sourceMap!.get(id)!, id: Number(id)};
+  }).filter(Boolean)
+    .sort((a, b) => a.id - b.id);
+
+  return [...props.fixedTools, ...els];
+});
+
+const formState = inject(props.providerKey) as Record<string, any>;
+watch(toolList, function(oldValue, newValue) {
+  /**
+   * 新元素如果有缺少的需要将缺少的内容初始化数据
+   *
+   * [用户名称, 角色名称, 手机号]
+   *
+   * [用户名称,角色名称, 电话, 邮箱]
+   *
+   * 以上情况需要充值手机号的值并且触发查询
+   *
+   * [用户名称, 角色名称]
+   *
+   * [用户名称,角色名称, 电话, 邮箱]
+   *
+   * 以上情况不触重置查询
+   */
+
+  const diffEls = differenceBy(newValue, oldValue, 'id');
+
+  if (diffEls.length) {
+    diffEls.forEach(function({type, name}) {
+      switch (type) {
+        case 'date':
+          formState[name[0] as string] = null;
+          formState[name[1] as string] = null;
+          break;
+        case 'field':
+        case 'select':
+        default:
+          formState[name as string] = '';
+          break;
+      }
+    });
+    props.onSubmit();
+  }
+});
+// #endregion
+
+// #region 筛选Modal
+const transferVisible = ref(false);
+
+const transferOptions = computed(function() {
+  const data: {value: string; label: string}[] = [];
+
+  if (!props.sourceMap) return data;
+
+  props.sourceMap.forEach(function({label}, id) {
+    data.push({value: String(id), label});
+  });
+
+  return data;
+});
+
+const transferValue = ref<string[]>([]);
+
+watchEffect(function() {
+  if (!props.sourceTools) return;
+
+  transferValue.value = props.sourceTools.split(',');
+});
+
+function onTransferConfirm(e: Event) {
+  e.preventDefault();
+
+  transferVisible.value = false;
+  props.onFilterConfirm?.(transferValue.value.join(','));
+}
+
+const onFilter = props.onFilterConfirm
+  ? () => {
+    transferVisible.value = true;
+  }
+  : void 0;
+
+// #endregion
+</script>
+
+<template>
+  <LDFilterWrapper
+    @submit="props.onSubmit"
+    @reset="props.onReset"
+    @filter="onFilter"
+    :isSearching="props.isSearching"
+  >
+    <template v-for="state in toolList" :key="state.name.toString()">
+      <LDFilterInput
+        v-if="state.type === 'field'"
+        :proiderKey="props.providerKey"
+        :name="(state.name as string)"
+        :label="state.label"
+      />
+      <LDFilterDate
+        v-if="state.type === 'date'"
+        :providerKey="props.providerKey"
+        :name="(state.name as [string, string])"
+        :label="state.label"
+      />
+      <LDFilterSelect
+        v-if="state.type === 'select'"
+        :providerKey="props.providerKey"
+        :name="(state.name as string)"
+        :options="state.options.value"
+        :label="state.label"
+        :loading="state.loading?.value"
+        @search="state.onSearch"
+      />
+    </template>
+  </LDFilterWrapper>
+
+  <LDNormalModal
+    v-model="transferVisible"
+    title="选择筛选条件"
+    @submit="onTransferConfirm"
+    v-if="Boolean(onFilterConfirm)"
+  >
+    <NTransfer :options="transferOptions" v-model:value="transferValue" />
+  </LDNormalModal>
+</template>

+ 24 - 0
src/components/filter/group/state.ts

@@ -0,0 +1,24 @@
+import {Ref} from 'vue';
+
+export type LDFilterTool<T extends Record<string, unknown>> =
+| {
+  type: 'field';
+  label: string;
+  name: keyof T;
+}
+| {
+  type: 'date';
+  label: string;
+  name: [keyof T, keyof T];
+}
+| {
+  type: 'select';
+  label: string;
+  options: Ref<{label: string, value: string}[]>;
+  loading?: Ref<boolean>;
+  onSearch?: (value: string) => void;
+  name: keyof T;
+};
+
+export type LDFilterToolMap<T extends Record<string, unknown>>
+= Map<string, LDFilterTool<T>>;

+ 11 - 0
src/components/filter/hooks.ts

@@ -0,0 +1,11 @@
+import {computed, inject} from 'vue';
+
+export function useField<T>(key: symbol, name: string) {
+  const injectValue = inject(key) as Record<string, T>;
+
+  function onChange(value: T) {
+    injectValue[name] = value;
+  }
+
+  return [computed(() => injectValue[name]), onChange] as const;
+}

+ 30 - 0
src/components/filter/index.css

@@ -0,0 +1,30 @@
+.ld-filter-field-wrapper {
+  --border-radius: 6px;
+
+  display: flex;
+  gap: 8px;
+  align-items: center;
+
+  & label {
+    display: block;
+    width: 5em;
+  }
+
+  & .n-input {
+    --n-border-radius: var(--border-radius) !important;
+
+    flex: 1;
+  }
+
+  & .n-select {
+    flex: 1;
+
+    & .n-base-selection {
+      --n-border-radius: var(--border-radius) !important;
+    }
+  }
+
+  & .n-date-picker {
+    flex: 1;
+  }
+}

+ 6 - 0
src/components/filter/index.ts

@@ -0,0 +1,6 @@
+export {default as LDFilterWrapper} from './wrapper';
+export {default as LDFilterInput} from './input';
+export {default as LDFilterSelect} from './select';
+export {default as LDFilterDate} from './date';
+export {default as LDFilterGroup} from './group/index.vue';
+export type * from './group/state';

+ 42 - 0
src/components/filter/input/index.tsx

@@ -0,0 +1,42 @@
+import '../index.css';
+import {PropType, defineComponent} from 'vue';
+import {NInput} from 'naive-ui';
+import {useField} from '../hooks';
+
+export default defineComponent(
+  {
+    name: 'LDInput',
+    props: {
+      proiderKey: {
+        type: Symbol,
+        required: true,
+      },
+      name: {
+        type: String as PropType<string>,
+        required: true,
+      },
+      label: String as PropType<string>,
+    },
+    setup(props) {
+      const [value, setValue] = useField<string>(props.proiderKey, props.name);
+
+      return {value, setValue};
+    },
+    render() {
+      const {name, label} = this.$props;
+      const {value, setValue} = this;
+
+      return (
+        <div class="ld-filter-field-wrapper">
+          <label for={name}>{label}</label>
+          <NInput
+            clearable
+            class="ld-filter-input"
+            value={value}
+            onUpdateValue={setValue}
+          />
+        </div>
+      );
+    },
+  },
+);

+ 88 - 0
src/components/filter/select/index.tsx

@@ -0,0 +1,88 @@
+import '../index.css';
+import {PropType, computed, defineComponent, ref} from 'vue';
+import {selectProps, NSelect} from 'naive-ui';
+import {debounce} from 'lodash-es';
+import {useField} from '../hooks';
+
+export default defineComponent({
+  name: 'LDFilterSelect',
+  props: {
+    name: {
+      type: String as PropType<string>,
+      required: true,
+    },
+    label: {
+      type: String as PropType<string>,
+      required: true,
+    },
+    providerKey: {
+      type: Symbol,
+      required: true,
+    },
+    loading: selectProps.loading,
+    options: selectProps.options,
+    onSearch: selectProps.onSearch,
+  },
+  setup(props) {
+    const [value, setValue] = useField<string>(props.providerKey, props.name);
+
+    const filterValue = ref<string>('');
+    const filterOptions = computed(function() {
+      const {options} = props;
+
+      if (!filterValue.value) return options;
+
+      return options.filter(function(val) {
+        return (val?.label as string ?? '').includes(filterValue.value)
+         || (val?.value as string ?? '').includes(filterValue.value);
+      });
+    });
+    const onDefaultSearch = debounce(
+      function(value: string) {
+        filterValue.value = value;
+      },
+      0,
+      {
+        trailing: true,
+        leading: false,
+      },
+    );
+    // 失去焦点之后判断是否有搜索结果
+    // 如果没有搜索结果将搜索值清空
+    // 防止没搜索到之后失去焦点再返回会显示空选项
+    function onBlur() {
+      filterOptions.value.length === 0
+      && filterValue.value.length > 0
+      && (filterValue.value = '');
+    }
+
+    return {value, setValue, onDefaultSearch, filterOptions, onBlur};
+  },
+  render() {
+    const {label, onSearch, loading} = this.$props;
+    const {value, setValue, filterOptions, onDefaultSearch, onBlur} = this;
+
+    return (
+      <div class="ld-filter-field-wrapper">
+        <label>{label}</label>
+        <NSelect
+          clearFilterAfterSelect={false}
+          clearable
+          filterable
+          remote
+          loading={loading}
+          onClear={() => {
+            setValue('');
+            onDefaultSearch('');
+          }}
+          options={filterOptions}
+          // 防止空字符串没有placeholder
+          value={value || null}
+          onUpdateValue={setValue}
+          onSearch={onSearch ?? onDefaultSearch}
+          onBlur={onBlur}
+        />
+      </div>
+    );
+  },
+});

+ 12 - 0
src/components/filter/wrapper/index.css

@@ -0,0 +1,12 @@
+.ld-filter-wrapper {
+  --n-padding-left: var(--content-padding) !important;
+  --n-padding-right: var(--content-padding) !important;
+  --n-padding-bottom: var(--content-padding) !important;
+  --n-padding-top: var(--content-padding) !important;
+}
+
+.ld-filter-group-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+}

+ 42 - 0
src/components/filter/wrapper/index.tsx

@@ -0,0 +1,42 @@
+import './index.css';
+import {defineComponent, SlotsType, type PropType} from 'vue';
+import {NCard} from 'naive-ui';
+import LDButtonGroup from '../button-group';
+
+export default defineComponent({
+  name: 'LDFilterWrapper',
+  props: {
+    onSubmit: {
+      type: Function as PropType<(event: Event) => any>,
+      required: true,
+
+    },
+    onReset: {
+      type: Function as PropType<(event: Event) => any>,
+      require: true,
+    },
+    onFilter: Function as PropType<() => void>,
+    isSearching: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  slots: Object as SlotsType<{
+    default: string | undefined;
+  }>,
+  render() {
+    const {onSubmit, onReset, onFilter, isSearching} = this.$props;
+    const slots = this.$slots;
+
+    return (
+      <NCard class="ld-filter-wrapper" >
+        <form onSubmit={onSubmit} onReset={onReset}>
+          <div class="ld-filter-group-grid">
+            {slots.default?.()}
+            <LDButtonGroup onFilter={onFilter} isSearching={isSearching}/>
+          </div>
+        </form>
+      </NCard>
+    );
+  },
+});

+ 2 - 0
src/components/index.ts

@@ -1,2 +1,4 @@
 export * from './login-field';
 export * from './login-field';
 export * from './button';
 export * from './button';
+export * from './filter';
+export * from './modal';

+ 1 - 0
src/components/modal/index.ts

@@ -0,0 +1 @@
+export {default as LDNormalModal} from './normal';

+ 133 - 0
src/components/modal/normal/index.css

@@ -0,0 +1,133 @@
+.ld-normal-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+}
+
+.ld-normal-modal {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 680px;
+  height: 600px;
+  padding: 0 12px;
+  background-color: #fff;
+  border-radius: 8px;
+}
+
+.ld-normal-modal-close {
+  position: absolute;
+  top: 16px;
+  right: 22px;
+  z-index: 2;
+  width: 24px;
+  height: 24px;
+  font-size: 24px;
+  color: #d7d7d7;
+  cursor: pointer;
+  transition: color 100ms linear;
+
+  &:hover {
+    color: #999;
+  }
+}
+
+.ld-normal-modal-title {
+  z-index: 1;
+  width: 100%;
+  padding: 46px 0 12px;
+  font-size: 20px;
+  text-align: center;
+
+  & h3 {
+    font-weight: normal;
+  }
+}
+
+.ld-normal-modal-decorate {
+  position: absolute;
+  top: -40px;
+  left: calc(50% - 56px);
+  z-index: 2;
+  display: flex;
+  justify-content: space-between;
+  width: 120px;
+  height: 80px;
+}
+
+.ld-normal-modal-decorate-icon1 {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  border: 2px solid var(--primary-color);
+  border-radius: 50%;
+  transform: translateY(16px);
+}
+
+.ld-normal-modal-decorate-circle {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 80px;
+  height: 80px;
+  background-image: linear-gradient(180deg, var(--primary-color), var(--primary-color-5));
+  border: 6px solid white;
+  border-radius: 50px;
+
+  & img {
+    width: 36px;
+    height: 36px;
+  }
+}
+
+.ld-normal-modal-decorate-icon2 {
+  width: 16px;
+  height: 16px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  opacity: 0.6;
+}
+
+.ld-normal-modal-form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: calc(100% - 40px);
+  height: calc(100% - 89px);
+  padding: 16px 30px 30px;
+  overflow: hidden;
+}
+
+.ld-normal-modal-form-content {
+  flex: 1;
+  width: 100%;
+  height: calc(100% - 50px);
+  overflow-x: hidden;
+  overflow-y: auto;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: #ededed;
+    border-radius: 10px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, var(--primary-color), var(--primary-color-5));
+    border-radius: 10px;
+  }
+}
+
+.ld-normal-modal-button {
+  width: 234px;
+  height: 40px;
+  margin-top: 20px;
+  line-height: 46px;
+}

+ 80 - 0
src/components/modal/normal/index.tsx

@@ -0,0 +1,80 @@
+import './index.css';
+import {PropType, defineComponent} from 'vue';
+import {VueFinalModal} from 'vue-final-modal';
+import {CloseOne} from '@icon-park/vue-next';
+import addIcon from '@assets/images/modal/add.svg';
+import editIcon from '@assets/images/modal/edit.svg';
+import {LDButton} from '@components';
+import {useI18n} from 'vue-i18n';
+
+export default defineComponent(
+  {
+    name: 'LDNormalModal',
+    props: {
+      modelValue: {
+        type: Boolean as PropType<boolean>,
+        required: true,
+      },
+      title: {
+        type: String,
+        required: true,
+      },
+      isAdd: Boolean,
+      icon: String,
+      onSubmit: Function as PropType<(e: Event) => void>,
+    },
+    emits: {
+      'update:modelValue': Boolean,
+    },
+    setup(props, {slots, emit}) {
+      const event = {
+        'onUpdate:modelValue': (value: boolean) => emit('update:modelValue', value),
+      };
+      const {t} = useI18n();
+
+      function onClose() {
+        emit('update:modelValue', false);
+      }
+
+      return function() {
+        return (
+          <VueFinalModal
+            {...event}
+            modelValue={props.modelValue}
+          >
+            <div class="ld-normal-wrapper ">
+              <div class="ld-normal-modal">
+                <div onClick={onClose} class="ld-normal-modal-close">
+                  <CloseOne />
+                </div>
+
+                <div class="ld-normal-modal-title">
+                  <h3>{props.title}</h3>
+                </div>
+
+                <div class="ld-normal-modal-decorate">
+                  <span class="ld-normal-modal-decorate-icon1" />
+                  <div class="ld-normal-modal-decorate-circle">
+                    <img src={
+                      props.icon ?? (props.isAdd ? addIcon : editIcon)
+                    } />
+                  </div>
+                  <span class="ld-normal-modal-decorate-icon2" />
+                </div>
+
+                <form onSubmit={props.onSubmit} class="ld-normal-modal-form">
+                  <div class="ld-normal-modal-form-content">
+                    {slots.default?.()}
+                  </div>
+                  <LDButton class="ld-normal-modal-button" attrType="submit">
+                    {t('common.confirm')}
+                  </LDButton>
+                </form>
+              </div>
+            </div>
+          </VueFinalModal>
+        );
+      };
+    },
+  },
+);

+ 9 - 1
src/locales/common.ts

@@ -1,4 +1,12 @@
 export default {
 export default {
-  zh: {title: '供应商管理系统'},
+  zh: {
+    title: '供应商管理系统',
+    filter: {
+      search: '查询',
+      reset: '重置',
+      filter: '筛选',
+    },
+    confirm: '确定',
+  },
   ko: {title: '供应商管理系统'},
   ko: {title: '供应商管理系统'},
 };
 };

+ 1 - 0
src/main.ts

@@ -4,6 +4,7 @@ import '@styles/naiveui.css';
 import '@icon-park/vue-next/styles/index.css';
 import '@icon-park/vue-next/styles/index.css';
 import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css';
 import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css';
 import '@styles/contextMenu.css';
 import '@styles/contextMenu.css';
+import 'vue-final-modal/style.css';
 
 
 import {createApp} from 'vue';
 import {createApp} from 'vue';
 import App from './App.vue';
 import App from './App.vue';

+ 0 - 5
src/models/.gitkeep

@@ -1,5 +0,0 @@
-# To ensure that empty folders can also be submitted
-
-# You can delete this file
-
-# This type of file has been entered in .gitignore, and this type of file will not be submitted to git

+ 2 - 0
src/models/index.ts

@@ -0,0 +1,2 @@
+export * from './request';
+export * from './response';

+ 10 - 0
src/models/request/index.ts

@@ -0,0 +1,10 @@
+export type ListParams = {
+  /** 页码 */
+  page: string;
+  /** 每页数量 */
+  limit: string;
+};
+export type OriginalListParams<T extends ListParams> = Omit<
+T,
+'page' | 'limit'
+>;

+ 30 - 0
src/models/response/index.ts

@@ -0,0 +1,30 @@
+export type BaseResultContent<T> =
+  | {msg: '200'; data: T}
+  | {msg: '510'; errMsg: string};
+
+export type BaseResult<T = any> = Promise<BaseResultContent<T>>;
+
+export type BaseListData<T> = {
+  total: number;
+  list: T[];
+  pageNum: number;
+  pageSize: number;
+  size: number;
+  startRow: number;
+  endRow: number;
+  pages: number;
+  prePage: number;
+  nextPage: number;
+  isFirstPage: boolean;
+  isLastPage: boolean;
+  hasPreviousPage: boolean;
+  hasNextPage: boolean;
+  navigatePages: number;
+  navigatepageNums: any[];
+  navigateFirstPage: number;
+  navigateLastPage: number;
+};
+
+export type BaseListResult<T = any> = BaseResult<BaseListData<T>>;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export type GetBaseListType<T> = T extends BaseListResult<infer T> ? T : never;

+ 36 - 0
src/pages/audit/filter/hooks.ts

@@ -0,0 +1,36 @@
+import {klona} from 'klona/json';
+import {provide, reactive, ref} from 'vue';
+
+export function useSearch<T extends Record<string, unknown>>(
+  initialValues: T,
+  options?: {
+    initFilter?: string,
+  },
+) {
+  const memoInit = klona(initialValues);
+  const providerValue = reactive(initialValues);
+  const key = Symbol('filterSymbol');
+
+  provide(key, providerValue);
+
+  function onSubmit(e?: Event) {
+    e?.preventDefault();
+  }
+
+  function onReset(e: Event) {
+    e.preventDefault();
+    Object.assign(providerValue, memoInit);
+  }
+
+  const {initFilter} = options ?? {};
+  const filterSource = ref(initFilter ?? '');
+
+  function onFilterConfirm(value: string) {
+    filterSource.value = value;
+  }
+
+  return [
+    {key, filterSource},
+    {onSubmit, onReset, onFilterConfirm},
+  ] as const;
+}

+ 69 - 0
src/pages/audit/filter/index.vue

@@ -0,0 +1,69 @@
+<script setup lang='ts'>
+import {PATH_NAME} from '../state';
+import {LDFilterGroup, type LDFilterTool} from '@components';
+import {useSearch} from './hooks';
+import {ref} from 'vue';
+
+defineOptions({name: PATH_NAME + 'Filter'});
+
+const mockInitValue = {
+  type: '',
+  name: '',
+  startTime: null,
+  endTime: null,
+  com: '',
+  no: '',
+};
+
+const mockOptions = ref([
+  {label: '类型1', value: '111'},
+  {label: '类型2', value: '222'},
+  {label: '类型3', value: '333'},
+]);
+
+const mockLoading = ref(true);
+
+setTimeout(function() {
+  mockLoading.value = false;
+  mockOptions.value = [
+    ...mockOptions.value,
+    {label: '类型4', value: '444'},
+    {label: '类型5', value: '555'},
+    {label: '类型6', value: '666'},
+  ];
+}, 2000);
+
+const tools: LDFilterTool<typeof mockInitValue>[] = [
+  {type: 'field', label: '名称', name: 'name'},
+  {type: 'select', label: '类型', name: 'type', options: mockOptions, loading: mockLoading},
+  {type: 'date', label: '发货时间', name: ['startTime', 'endTime']},
+];
+
+const mockFilterMap = new Map<string, LDFilterTool<typeof mockInitValue>>(
+  [
+    ['1', {type: 'field', label: '公司', name: 'com'}],
+    ['2', {type: 'field', label: '编号', name: 'no'}],
+  ],
+);
+
+const [{key, filterSource}, {onSubmit, onReset, onFilterConfirm}] = useSearch(
+  mockInitValue,
+  {initFilter: ''},
+);
+
+const isSearch = ref(false);
+</script>
+
+<template>
+  <LDFilterGroup
+    @submit="onSubmit"
+    @reset="onReset"
+    @filterConfirm="onFilterConfirm"
+    :isSearching="isSearch"
+    :providerKey="key"
+    :fixedTools="tools"
+    :sourceTools="filterSource"
+    :sourceMap="mockFilterMap"
+  />
+</template>
+

+ 11 - 0
src/pages/audit/index.vue

@@ -0,0 +1,11 @@
+<script setup lang='ts'>
+import {PATH_NAME} from './state';
+import Filter from './filter/index.vue';
+
+defineOptions({name: PATH_NAME});
+</script>
+
+<template>
+  <Filter />
+</template>
+

+ 3 - 0
src/pages/audit/state.ts

@@ -0,0 +1,3 @@
+import {AUDIT_PATH, RouteNameMap} from '@routes';
+
+export const PATH_NAME = RouteNameMap.get(AUDIT_PATH);

+ 15 - 0
src/pages/home/index.css

@@ -43,3 +43,18 @@ header {
   overflow: hidden;
   overflow: hidden;
   background-color: var(--background-color);
   background-color: var(--background-color);
 }
 }
+
+.content-info {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+.content-main {
+  flex: 1;
+  width: 100%;
+  padding: var(--content-padding);
+  overflow: hidden;
+}

+ 7 - 1
src/pages/home/index.vue

@@ -6,6 +6,7 @@ import Local from './local/index.vue';
 import Menu from './menu/index.vue';
 import Menu from './menu/index.vue';
 import {NLayout, NLayoutContent} from 'naive-ui';
 import {NLayout, NLayoutContent} from 'naive-ui';
 import TabList from './tab/index.vue';
 import TabList from './tab/index.vue';
+import {RouterView} from 'vue-router';
 
 
 defineOptions({
 defineOptions({
   name: 'Home',
   name: 'Home',
@@ -28,7 +29,12 @@ defineOptions({
     <NLayout class="content" hasSider>
     <NLayout class="content" hasSider>
       <Menu />
       <Menu />
       <NLayoutContent>
       <NLayoutContent>
-        <TabList />
+        <section class="content-info">
+          <TabList />
+          <div class="content-main">
+            <RouterView />
+          </div>
+        </section>
       </NLayoutContent>
       </NLayoutContent>
     </NLayout>
     </NLayout>
   </div>
   </div>

+ 1 - 0
src/reset.d.ts

@@ -0,0 +1 @@
+import '@total-typescript/ts-reset';

+ 21 - 2
src/routes/index.ts

@@ -1,5 +1,7 @@
 import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router';
 import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router';
 import {
 import {
+  AUDIT_NAME,
+  AUDIT_PATH,
   HOME_NAME,
   HOME_NAME,
   HOME_PATH,
   HOME_PATH,
   LOGIN_NAME,
   LOGIN_NAME,
@@ -11,9 +13,26 @@ import Home from '@pages/home/index.vue';
 import Login from '@pages/login/index.vue';
 import Login from '@pages/login/index.vue';
 
 
 const routes: RouteRecordRaw[] = [
 const routes: RouteRecordRaw[] = [
+  {
+    path: HOME_PATH,
+    component: Home,
+    name: HOME_NAME,
+    children: [
+
+      {
+        path: AUDIT_PATH,
+        name: AUDIT_NAME,
+        component: () => import('@pages/audit/index.vue'),
+      },
+    ],
+  },
   {path: LOGIN_PATH, component: Login, name: LOGIN_NAME},
   {path: LOGIN_PATH, component: Login, name: LOGIN_NAME},
-  {path: HOME_PATH, component: Home, name: HOME_NAME},
-  {path: REGISTER_PATH, name: REGISTER_NAME, component: () => import('@pages/register/index.vue')},
+  {
+    path: REGISTER_PATH,
+    name: REGISTER_NAME,
+    component: () => import('@pages/register/index.vue'),
+  },
+
 ];
 ];
 
 
 export const router = createRouter({
 export const router = createRouter({

+ 7 - 0
src/routes/name.ts

@@ -1,3 +1,5 @@
+export const RouteNameMap = new Map();
+
 /** 首页 */
 /** 首页 */
 export const HOME_PATH = '/';
 export const HOME_PATH = '/';
 export const HOME_NAME = Symbol('HOME_NAME');
 export const HOME_NAME = Symbol('HOME_NAME');
@@ -9,3 +11,8 @@ export const LOGIN_NAME = Symbol('LOGIN_NAME');
 /** 注册界面 */
 /** 注册界面 */
 export const REGISTER_PATH = '/register';
 export const REGISTER_PATH = '/register';
 export const REGISTER_NAME = Symbol('REGISTER_NAME');
 export const REGISTER_NAME = Symbol('REGISTER_NAME');
+
+/** 审核列表 */
+export const AUDIT_PATH = '/audit';
+export const AUDIT_NAME = Symbol('Audit');
+RouteNameMap.set(AUDIT_PATH, 'AuditPage');

+ 18 - 1
src/styles/variable.css

@@ -1,5 +1,6 @@
 :root {
 :root {
-  /* theme */
+  /* 主色 */
+  --primary-color-1: #0f2572;
   --primary-color: #1e3588;
   --primary-color: #1e3588;
   --primary-color-2: #263e94;
   --primary-color-2: #263e94;
   --primary-color-3: #3048a0;
   --primary-color-3: #3048a0;
@@ -9,7 +10,20 @@
   --primary-color-7: #9ba6d3;
   --primary-color-7: #9ba6d3;
   --primary-color-8: #c2c9e5;
   --primary-color-8: #c2c9e5;
   --primary-color-9: #e7e9f4;
   --primary-color-9: #e7e9f4;
+
+  /* 辅助色 */
+  --accent-color-1: #f96e13;
+  --accent-color-2: #fb8d15;
+  --accent-color-3: #fb9d16;
   --accent-color: #fcaf17;
   --accent-color: #fcaf17;
+  --accent-color-5: #fcbd1c;
+  --accent-color-6: #fdc730;
+  --accent-color-7: #fed253;
+  --accent-color-8: #fede84;
+  --accent-color-9: #ffebb4;
+  --accent-color-10: #fff7e1;
+
+  /* 其他颜色 */
   --background-color: #f5f5f5;
   --background-color: #f5f5f5;
   --layout-background-color: white;
   --layout-background-color: white;
   --border-color: '#eee';
   --border-color: '#eee';
@@ -25,4 +39,7 @@
   --subtitle-font-color: #333;
   --subtitle-font-color: #333;
   --font-color: #666;
   --font-color: #666;
   --tip-font-color: #999;
   --tip-font-color: #999;
+
+  /* edge */
+  --content-padding: 24px;
 }
 }

+ 5 - 0
src/utils/constants.ts

@@ -0,0 +1,5 @@
+/** 请求域名 */
+export const NETWORK_URL
+  = process.env.NODE_ENV === 'development'
+    ? 'http://jsonplaceholder.typicode.com'
+    : 'http://10.2.111.91:9560';

+ 1 - 0
src/utils/index.ts

@@ -1,2 +1,3 @@
 export * from './validate-tips';
 export * from './validate-tips';
 export * from './theme';
 export * from './theme';
+export * from './constants';

+ 20 - 2
src/utils/theme.ts

@@ -1,4 +1,4 @@
-const theme = {
+const primaryTheme = {
   primaryColor: '#1e3588',
   primaryColor: '#1e3588',
   primaryColor2: '#263e94',
   primaryColor2: '#263e94',
   primaryColor3: '#3048a0',
   primaryColor3: '#3048a0',
@@ -8,6 +8,22 @@ const theme = {
   primaryColor7: '#9ba6d3',
   primaryColor7: '#9ba6d3',
   primaryColor8: '#c2c9e5',
   primaryColor8: '#c2c9e5',
   primaryColor9: '#e7e9f4',
   primaryColor9: '#e7e9f4',
+} as const;
+
+const accentTheme = {
+  accentColor1: '#f96e13',
+  accentColor2: '#fb8d15',
+  accentColor3: '#fb9d16',
+  accentColor: '#fcaf17',
+  accentColor5: '#fcbd1c',
+  accentColor6: '#fdc730',
+  accentColor7: '#fed253',
+  accentColor8: '#fede84',
+  accentColor9: '#ffebb4',
+  accentColor10: '#fff7e1',
+} as const;
+
+const otherTheme = {
   backgroundColor: 'f5f5f5',
   backgroundColor: 'f5f5f5',
   layoutBackgroundColor: '#ffffff',
   layoutBackgroundColor: '#ffffff',
 } as const;
 } as const;
@@ -26,6 +42,8 @@ const font = {
 } as const;
 } as const;
 
 
 export const lightVariable = {
 export const lightVariable = {
-  ...theme,
+  ...primaryTheme,
+  ...accentTheme,
+  ...otherTheme,
   ...font,
   ...font,
 };
 };

+ 0 - 1
vite.config.ts

@@ -30,7 +30,6 @@ export default defineConfig({
         assetFileNames: 'assets/[name].[hash].[ext]',
         assetFileNames: 'assets/[name].[hash].[ext]',
         manualChunks: {
         manualChunks: {
           vueVendor: ['vue', 'vue-router'],
           vueVendor: ['vue', 'vue-router'],
-          ldComponents: ['src/components/*'],
         },
         },
       },
       },
     },
     },