feat: 优化表格逻辑
parent
aac4047c9a
commit
6a000652b1
|
|
@ -40,6 +40,7 @@
|
|||
"unocss": "^0.49.8",
|
||||
"unplugin-auto-import": "^0.13.0",
|
||||
"unplugin-vue-components": "^0.23.0",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pages": "^0.28.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
|
|
|
|||
273
pnpm-lock.yaml
273
pnpm-lock.yaml
|
|
@ -85,6 +85,9 @@ devDependencies:
|
|||
unplugin-vue-components:
|
||||
specifier: ^0.23.0
|
||||
version: 0.23.0(vue@3.3.4)
|
||||
unplugin-vue-router:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0(vue-router@4.2.4)(vue@3.3.4)
|
||||
vite:
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9(less@4.2.0)
|
||||
|
|
@ -170,10 +173,10 @@ packages:
|
|||
'@babel/helper-compilation-targets': 7.22.15
|
||||
'@babel/helper-module-transforms': 7.22.17(@babel/core@7.22.17)
|
||||
'@babel/helpers': 7.22.15
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/parser': 7.23.6
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/traverse': 7.22.17
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
convert-source-map: 1.9.0
|
||||
debug: 4.3.4
|
||||
gensync: 1.0.0-beta.2
|
||||
|
|
@ -187,7 +190,7 @@ packages:
|
|||
resolution: {integrity: sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
'@jridgewell/gen-mapping': 0.3.3
|
||||
'@jridgewell/trace-mapping': 0.3.19
|
||||
jsesc: 2.5.2
|
||||
|
|
@ -197,7 +200,7 @@ packages:
|
|||
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-compilation-targets@7.22.15:
|
||||
|
|
@ -239,28 +242,28 @@ packages:
|
|||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-hoist-variables@7.22.5:
|
||||
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-member-expression-to-functions@7.22.15:
|
||||
resolution: {integrity: sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-imports@7.22.15:
|
||||
resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-transforms@7.22.17(@babel/core@7.22.17):
|
||||
|
|
@ -281,7 +284,7 @@ packages:
|
|||
resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-plugin-utils@7.22.5:
|
||||
|
|
@ -305,21 +308,21 @@ packages:
|
|||
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-skip-transparent-expression-wrappers@7.22.5:
|
||||
resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration@7.22.6:
|
||||
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/helper-string-parser@7.22.5:
|
||||
|
|
@ -327,6 +330,11 @@ packages:
|
|||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-string-parser@7.23.4:
|
||||
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier@7.22.20:
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -343,7 +351,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/traverse': 7.22.17
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
|
@ -365,6 +373,14 @@ packages:
|
|||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.23.6:
|
||||
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.17):
|
||||
resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -408,8 +424,8 @@ packages:
|
|||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.13
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/parser': 7.23.6
|
||||
'@babel/types': 7.23.6
|
||||
dev: true
|
||||
|
||||
/@babel/traverse@7.22.17:
|
||||
|
|
@ -422,8 +438,8 @@ packages:
|
|||
'@babel/helper-function-name': 7.22.5
|
||||
'@babel/helper-hoist-variables': 7.22.5
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/parser': 7.23.6
|
||||
'@babel/types': 7.23.6
|
||||
debug: 4.3.4
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -439,6 +455,15 @@ packages:
|
|||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/types@7.23.6:
|
||||
resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.23.4
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@esbuild-kit/cjs-loader@2.4.2:
|
||||
resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==}
|
||||
dependencies:
|
||||
|
|
@ -993,6 +1018,20 @@ packages:
|
|||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@rollup/pluginutils@5.1.0:
|
||||
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/estree': 1.0.1
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@sindresorhus/is@5.6.0:
|
||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
|
@ -1106,7 +1145,7 @@ packages:
|
|||
hasBin: true
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@unocss/config': 0.49.8
|
||||
'@unocss/core': 0.49.8
|
||||
'@unocss/preset-uno': 0.49.8
|
||||
|
|
@ -1236,7 +1275,7 @@ packages:
|
|||
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.2.1
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@unocss/config': 0.49.8
|
||||
'@unocss/core': 0.49.8
|
||||
'@unocss/inspector': 0.49.8
|
||||
|
|
@ -1295,6 +1334,26 @@ packages:
|
|||
'@volar/language-core': 1.10.1
|
||||
dev: true
|
||||
|
||||
/@vue-macros/common@1.10.0(vue@3.3.4):
|
||||
resolution: {integrity: sha512-4DZsPeQA/nBQDw2RkYAmH7KrFjJVrMdAhJhO1JCl1bbbFXCGeoGjXfkg9wHPppj47s2HpAB3GrqNwqVGbi12NQ==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.2.25
|
||||
peerDependenciesMeta:
|
||||
vue:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.6
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@vue/compiler-sfc': 3.3.11
|
||||
ast-kit: 0.11.3
|
||||
local-pkg: 0.5.0
|
||||
magic-string-ast: 0.3.0
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/@vue/babel-helper-vue-transform-on@1.1.5:
|
||||
resolution: {integrity: sha512-SgUymFpMoAyWeYWLAY+MkCK3QEROsiUnfaw5zxOVD/M64KQs8D/4oK6Q5omVA2hnvEOE0SCkH2TZxs/jnnUj7w==}
|
||||
dev: true
|
||||
|
|
@ -1309,7 +1368,7 @@ packages:
|
|||
'@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.17)
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/traverse': 7.22.17
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
'@vue/babel-helper-vue-transform-on': 1.1.5
|
||||
camelcase: 6.3.0
|
||||
html-tags: 3.3.1
|
||||
|
|
@ -1318,15 +1377,31 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-core@3.3.11:
|
||||
resolution: {integrity: sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/shared': 3.3.11
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-core@3.3.4:
|
||||
resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/shared': 3.3.4
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-dom@3.3.11:
|
||||
resolution: {integrity: sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==}
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.3.11
|
||||
'@vue/shared': 3.3.11
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-dom@3.3.4:
|
||||
resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
|
||||
dependencies:
|
||||
|
|
@ -1334,21 +1409,43 @@ packages:
|
|||
'@vue/shared': 3.3.4
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-sfc@3.3.11:
|
||||
resolution: {integrity: sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/compiler-core': 3.3.11
|
||||
'@vue/compiler-dom': 3.3.11
|
||||
'@vue/compiler-ssr': 3.3.11
|
||||
'@vue/reactivity-transform': 3.3.11
|
||||
'@vue/shared': 3.3.11
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.32
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-sfc@3.3.4:
|
||||
resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/compiler-core': 3.3.4
|
||||
'@vue/compiler-dom': 3.3.4
|
||||
'@vue/compiler-ssr': 3.3.4
|
||||
'@vue/reactivity-transform': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.3
|
||||
postcss: 8.4.29
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.32
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-ssr@3.3.11:
|
||||
resolution: {integrity: sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==}
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.3.11
|
||||
'@vue/shared': 3.3.11
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-ssr@3.3.4:
|
||||
resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==}
|
||||
dependencies:
|
||||
|
|
@ -1370,23 +1467,33 @@ packages:
|
|||
dependencies:
|
||||
'@volar/language-core': 1.10.1
|
||||
'@volar/source-map': 1.10.1
|
||||
'@vue/compiler-dom': 3.3.4
|
||||
'@vue/compiler-dom': 3.3.11
|
||||
'@vue/reactivity': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
'@vue/shared': 3.3.11
|
||||
minimatch: 9.0.3
|
||||
muggle-string: 0.3.1
|
||||
typescript: 4.9.5
|
||||
vue-template-compiler: 2.7.14
|
||||
dev: true
|
||||
|
||||
/@vue/reactivity-transform@3.3.11:
|
||||
resolution: {integrity: sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/compiler-core': 3.3.11
|
||||
'@vue/shared': 3.3.11
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.5
|
||||
dev: true
|
||||
|
||||
/@vue/reactivity-transform@3.3.4:
|
||||
resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.16
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/compiler-core': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.3
|
||||
magic-string: 0.30.5
|
||||
dev: true
|
||||
|
||||
/@vue/reactivity@3.3.4:
|
||||
|
|
@ -1420,6 +1527,10 @@ packages:
|
|||
vue: 3.3.4
|
||||
dev: true
|
||||
|
||||
/@vue/shared@3.3.11:
|
||||
resolution: {integrity: sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==}
|
||||
dev: true
|
||||
|
||||
/@vue/shared@3.3.4:
|
||||
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
|
||||
dev: true
|
||||
|
|
@ -1616,6 +1727,28 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/ast-kit@0.11.3:
|
||||
resolution: {integrity: sha512-qdwwKEhckRk0XE22/xDdmU3v/60E8Edu4qFhgTLIhGGDs/PAJwLw9pQn8Rj99PitlbBZbYpx0k/lbir4kg0SuA==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
dependencies:
|
||||
'@babel/parser': 7.23.6
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
pathe: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/ast-kit@0.9.5:
|
||||
resolution: {integrity: sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.16
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
pathe: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/ast-types@0.13.4:
|
||||
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -1623,6 +1756,16 @@ packages:
|
|||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/ast-walker-scope@0.5.0:
|
||||
resolution: {integrity: sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.16
|
||||
ast-kit: 0.9.5
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
dev: true
|
||||
|
||||
/async-retry@1.3.3:
|
||||
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
||||
dependencies:
|
||||
|
|
@ -4182,6 +4325,14 @@ packages:
|
|||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/local-pkg@0.5.0:
|
||||
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
mlly: 1.4.2
|
||||
pkg-types: 1.0.3
|
||||
dev: true
|
||||
|
||||
/locate-path@2.0.0:
|
||||
resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -4290,6 +4441,13 @@ packages:
|
|||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/magic-string-ast@0.3.0:
|
||||
resolution: {integrity: sha512-0shqecEPgdFpnI3AP90epXyxZy9g6CRZ+SZ7BcqFwYmtFEnZ1jpevcV5HoyVnlDS9gCnc1UIg3Rsvp3Ci7r8OA==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
dependencies:
|
||||
magic-string: 0.30.3
|
||||
dev: true
|
||||
|
||||
/magic-string@0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
dependencies:
|
||||
|
|
@ -4317,6 +4475,13 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/magic-string@0.30.5:
|
||||
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/make-dir@2.1.0:
|
||||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4569,8 +4734,8 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid@3.3.6:
|
||||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||
/nanoid@3.3.7:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
|
@ -5281,7 +5446,16 @@ packages:
|
|||
resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.6
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postcss@8.4.32:
|
||||
resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
|
@ -6377,14 +6551,14 @@ packages:
|
|||
dependencies:
|
||||
acorn: 8.10.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.3
|
||||
magic-string: 0.30.5
|
||||
unplugin: 1.5.0
|
||||
dev: true
|
||||
|
||||
/unimport@2.2.4:
|
||||
resolution: {integrity: sha512-qMgmeEGqqrrmEtm0dqxMG37J6xBtrriqxq9hILvDb+e6l2F0yTnJomLoCCp0eghLR7bYGeBsUU5Y0oyiUYhViw==}
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
escape-string-regexp: 5.0.0
|
||||
fast-glob: 3.3.1
|
||||
local-pkg: 0.4.3
|
||||
|
|
@ -6402,11 +6576,11 @@ packages:
|
|||
/unimport@3.3.0:
|
||||
resolution: {integrity: sha512-3jhq3ZG5hFZzrWGDCpx83kjPzefP/EeuKkIO1T0MA4Zwj+dO/Og1mFvZ4aZ5WSDm0FVbbdVIRH1zKBG7c4wOpg==}
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
escape-string-regexp: 5.0.0
|
||||
fast-glob: 3.3.1
|
||||
local-pkg: 0.4.3
|
||||
magic-string: 0.30.3
|
||||
magic-string: 0.30.5
|
||||
mlly: 1.4.2
|
||||
pathe: 1.1.1
|
||||
pkg-types: 1.0.3
|
||||
|
|
@ -6517,6 +6691,33 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/unplugin-vue-router@0.7.0(vue-router@4.2.4)(vue@3.3.4):
|
||||
resolution: {integrity: sha512-ddRreGq0t5vlSB7OMy4e4cfU1w2AwBQCwmvW3oP/0IHQiokzbx4hd3TpwBu3eIAFVuhX2cwNQwp1U32UybTVCw==}
|
||||
peerDependencies:
|
||||
vue-router: ^4.1.0
|
||||
peerDependenciesMeta:
|
||||
vue-router:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@rollup/pluginutils': 5.0.5
|
||||
'@vue-macros/common': 1.10.0(vue@3.3.4)
|
||||
ast-walker-scope: 0.5.0
|
||||
chokidar: 3.5.3
|
||||
fast-glob: 3.3.1
|
||||
json5: 2.2.3
|
||||
local-pkg: 0.4.3
|
||||
mlly: 1.4.2
|
||||
pathe: 1.1.1
|
||||
scule: 1.0.0
|
||||
unplugin: 1.5.0
|
||||
vue-router: 4.2.4(vue@3.3.4)
|
||||
yaml: 2.3.2
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- vue
|
||||
dev: true
|
||||
|
||||
/unplugin@1.4.0:
|
||||
resolution: {integrity: sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==}
|
||||
dependencies:
|
||||
|
|
@ -6546,7 +6747,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.22.17
|
||||
'@babel/standalone': 7.22.17
|
||||
'@babel/types': 7.23.0
|
||||
'@babel/types': 7.23.6
|
||||
defu: 6.1.2
|
||||
jiti: 1.20.0
|
||||
mri: 1.2.0
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { env } from '@/config/env';
|
|||
* @see src/api/instance/instance.ts
|
||||
*/
|
||||
export const api = new Service({
|
||||
timeout: 2000,
|
||||
baseURL: env.apiPrefix,
|
||||
});
|
||||
|
||||
|
|
@ -17,12 +18,12 @@ export const api = new Service({
|
|||
*/
|
||||
addToastInterceptor(api.instance);
|
||||
|
||||
/**
|
||||
* 添加异常处理拦截器
|
||||
*/
|
||||
addExceptionInterceptor(api.instance, () => api.expireHandler?.());
|
||||
/**
|
||||
* 添加登陆令牌拦截器
|
||||
*/
|
||||
addAuthInterceptor(api.instance);
|
||||
|
||||
/**
|
||||
* 添加异常处理拦截器
|
||||
*/
|
||||
addExceptionInterceptor(api.instance, () => api.expireHandler?.());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export function addAuthInterceptor(axios: AxiosInstance) {
|
|||
if (userStore.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||
}
|
||||
// throw Error('dd');
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
|||
return res;
|
||||
},
|
||||
error => {
|
||||
console.log('res error', error);
|
||||
if (error.response) {
|
||||
const code = error.response.data?.code;
|
||||
if (expiredCodes.includes(code)) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
|
|||
return config;
|
||||
},
|
||||
error => {
|
||||
error.config.closeToast?.();
|
||||
error.config?.closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -37,7 +37,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
|
|||
return response;
|
||||
},
|
||||
error => {
|
||||
error.config.closeToast?.();
|
||||
error.config?.closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
<img src="@/assets/403.svg" alt="forbiden" class="w-[320px]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-3xl m-0 font-bold">403</h2>
|
||||
<p class="mt-2">权限不足,如需访问请联系管理员!</p>
|
||||
<h2 class="text-3xl m-0 font-medium">403</h2>
|
||||
<p class="mt-3">权限不足,如需访问请联系管理员!</p>
|
||||
<div class="space-x-3 mt-6">
|
||||
<a-button type="primary" @click="router.back()">
|
||||
<template #icon>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
import { Form, FormInstance } from '@arco-design/web-vue';
|
||||
import { Form, FormInstance, Message } from '@arco-design/web-vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { PropType } from 'vue';
|
||||
import { FormContextKey } from './useFormContext';
|
||||
import { useFormItems } from './useFormItems';
|
||||
import { useFormModel } from './useFormModel';
|
||||
import { useFormRef } from './useFormRef';
|
||||
import { useFormSubmit } from './useFormSubmit';
|
||||
import { ComputedRef, InjectionKey, PropType, Ref } from 'vue';
|
||||
import { initFormItems } from '../utils/useFormItems';
|
||||
import { FormRef, useFormRef } from '../utils/useFormRef';
|
||||
import { AnFormItem, AnFormItemProps } from './FormItem';
|
||||
import { cloneDeep, isFunction, isObject, merge } from 'lodash-es';
|
||||
import { getModel } from '../utils/useFormModel';
|
||||
|
||||
const SUBMIT_ITEM = {
|
||||
field: 'id',
|
||||
setter: 'submit' as const,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
};
|
||||
|
||||
export type FormContextInterface = FormRef & {
|
||||
model: Ref<Recordable>;
|
||||
items: ComputedRef<AnFormItemProps[]>;
|
||||
loading: Ref<boolean>;
|
||||
submitForm: any;
|
||||
resetForm: any;
|
||||
};
|
||||
|
||||
export const FormContextKey = Symbol('FormContextKey') as InjectionKey<FormContextInterface>;
|
||||
|
||||
/**
|
||||
* 表单组件
|
||||
|
|
@ -50,7 +67,7 @@ export const AnForm = defineComponent({
|
|||
* ```
|
||||
*/
|
||||
submit: {
|
||||
type: [String, Function, Object] as PropType<AnFormSubmit>,
|
||||
type: [Function, Object] as PropType<AnFormSubmit>,
|
||||
},
|
||||
/**
|
||||
* 传给Form组件的参数
|
||||
|
|
@ -69,25 +86,61 @@ export const AnForm = defineComponent({
|
|||
setup(props, { slots, emit }) {
|
||||
const model = useVModel(props, 'model', emit);
|
||||
const items = computed(() => props.items);
|
||||
const formRefes = useFormRef();
|
||||
const formModel = useFormModel(model, formRefes.clearValidate);
|
||||
const formItems = useFormItems(items, model);
|
||||
const formSubmit = useFormSubmit(props, formRefes.validate, formModel.getModel);
|
||||
const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit };
|
||||
const initModel = cloneDeep(model.value);
|
||||
const loading = ref(false);
|
||||
const { formRef, ...formMethods } = useFormRef();
|
||||
|
||||
const submitItem = () => {
|
||||
if (!props.submit) {
|
||||
return null;
|
||||
}
|
||||
if (isFunction(props.submit)) {
|
||||
return SUBMIT_ITEM;
|
||||
}
|
||||
if (isObject(props.submit)) {
|
||||
return merge({}, SUBMIT_ITEM, props.submit);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
model.value = cloneDeep(initModel);
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (await formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
const submit: any = typeof props.submit === 'object' ? props.submit.visible : props.submit;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = getModel(model.value);
|
||||
const res = await submit?.(data, props.items);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(`提示: ${msg}`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const context = { slots, loading, resetForm, submitForm, submitItem, model, items, formRef, ...formMethods };
|
||||
provide(FormContextKey, context);
|
||||
|
||||
onMounted(() => {
|
||||
initFormItems(props.items, model.value);
|
||||
});
|
||||
|
||||
return context;
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Form layout="vertical" {...this.$attrs} {...this.formProps} class="an-form" ref="formRef" model={this.model}>
|
||||
<Form layout="vertical" {...this.formProps} class="an-form" ref="formRef" model={this.model}>
|
||||
{this.items.map(item => (
|
||||
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
|
||||
))}
|
||||
{this.$slots.submit?.(this.model, this.validate) ||
|
||||
(this.submit && this.submitItem && (
|
||||
<AnFormItem item={this.submitItem} items={this.items} model={this.model}></AnFormItem>
|
||||
))}
|
||||
{this.submitItem()}
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
|
|
@ -99,4 +152,4 @@ export type AnFormProps = Pick<AnFormInstance['$props'], 'model' | 'items' | 'su
|
|||
|
||||
export type AnFormSubmitFn = (model: Recordable, items: AnFormItemProps[]) => any;
|
||||
|
||||
export type AnFormSubmit = string | AnFormSubmitFn;
|
||||
export type AnFormSubmit = AnFormSubmitFn | AnFormItemProps;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import { useVisible } from '@/hooks/useVisible';
|
||||
import { Button, ButtonInstance, FormInstance, Modal } from '@arco-design/web-vue';
|
||||
import { Button, ButtonInstance, FormInstance, Message, Modal } from '@arco-design/web-vue';
|
||||
import { InjectionKey, PropType, Ref } from 'vue';
|
||||
import { useModalSubmit } from './useModalSubmit';
|
||||
import { useModalTrigger } from './useModalTrigger';
|
||||
import { AnForm, AnFormInstance, AnFormProps, AnFormSubmit } from './Form';
|
||||
import { AnForm, AnFormInstance, AnFormSubmit } from './Form';
|
||||
import { AnFormItemProps } from './FormItem';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { getModel, setModel } from '../utils/useFormModel';
|
||||
|
||||
export interface AnFormModalContext {
|
||||
visible: Ref<boolean>;
|
||||
loading: Ref<boolean>;
|
||||
formRef: Ref<AnFormInstance | null>;
|
||||
anFormRef: Ref<AnFormInstance | null>;
|
||||
submitForm: () => any | Promise<any>;
|
||||
open: (data: Recordable) => void;
|
||||
close: () => void;
|
||||
submitForm: () => any | Promise<any>;
|
||||
modalTitle: () => any;
|
||||
modalTrigger: () => any;
|
||||
onClose: () => void;
|
||||
|
|
@ -97,7 +95,7 @@ export const AnFormModal = defineComponent({
|
|||
* ```
|
||||
*/
|
||||
submit: {
|
||||
type: [String, Function] as PropType<AnFormSubmit>,
|
||||
type: [Object, Function] as PropType<AnFormSubmit>,
|
||||
},
|
||||
/**
|
||||
* 传给Form组件的参数
|
||||
|
|
@ -114,25 +112,10 @@ export const AnFormModal = defineComponent({
|
|||
},
|
||||
emits: ['update:model', 'submited'],
|
||||
setup(props, { emit }) {
|
||||
const formRef = ref<AnFormInstance | null>(null);
|
||||
const model = useVModel(props, 'model', emit);
|
||||
const anFormRef = ref<AnFormInstance | null>(null);
|
||||
const visible = ref(false);
|
||||
const show = () => (visible.value = true);
|
||||
const hide = () => (visible.value = false);
|
||||
const modalTrigger = useModalTrigger(props, show);
|
||||
const { loading, setLoading, submitForm } = useModalSubmit(props, formRef, visible, emit, model);
|
||||
|
||||
const open = (data: Recordable = {}) => {
|
||||
formRef.value?.setModel(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setLoading(false);
|
||||
hide();
|
||||
};
|
||||
|
||||
const onClose = () => {};
|
||||
const loading = ref(false);
|
||||
|
||||
const modalTitle = () => {
|
||||
if (typeof props.title === 'string') {
|
||||
|
|
@ -141,10 +124,71 @@ export const AnFormModal = defineComponent({
|
|||
return <props.title model={props.model} items={props.items}></props.title>;
|
||||
};
|
||||
|
||||
const modalTrigger = () => {
|
||||
if (!props.trigger) {
|
||||
return null;
|
||||
}
|
||||
if (typeof props.trigger === 'function') {
|
||||
return <props.trigger model={props.model} items={props.items} open={open}></props.trigger>;
|
||||
}
|
||||
const internal = {
|
||||
text: '新增',
|
||||
buttonProps: {},
|
||||
buttonSlots: {},
|
||||
};
|
||||
if (typeof props.trigger === 'string') {
|
||||
internal.text = props.trigger;
|
||||
}
|
||||
if (typeof props.trigger === 'object') {
|
||||
Object.assign(internal, props.trigger);
|
||||
}
|
||||
return (
|
||||
<Button type="primary" {...internal.buttonProps} onClick={open}>
|
||||
{{
|
||||
...internal.buttonSlots,
|
||||
icon: () => <i class="icon-park-outline-add"></i>,
|
||||
default: () => internal.text,
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (await anFormRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = getModel(model.value);
|
||||
const res = await (props as any).submit?.(data, props.items);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(msg);
|
||||
visible.value = false;
|
||||
emit('submited', res);
|
||||
} catch {
|
||||
// todo
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const open = async (data: Recordable = {}) => {
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
anFormRef.value && setModel(model.value, data);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
loading.value = false;
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const onClose = () => {};
|
||||
|
||||
const context: AnFormModalContext = {
|
||||
visible,
|
||||
loading,
|
||||
formRef,
|
||||
anFormRef,
|
||||
open,
|
||||
close,
|
||||
onClose,
|
||||
|
|
@ -155,7 +199,9 @@ export const AnFormModal = defineComponent({
|
|||
|
||||
provide(AnFormModalContextKey, context);
|
||||
|
||||
return context;
|
||||
return {
|
||||
...context
|
||||
};
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
|
|
@ -164,11 +210,11 @@ export const AnFormModal = defineComponent({
|
|||
<Modal
|
||||
titleAlign="start"
|
||||
closable={false}
|
||||
{...this.$attrs}
|
||||
{...this.modalProps}
|
||||
v-model:visible={this.visible}
|
||||
class="an-form-modal"
|
||||
maskClosable={false}
|
||||
unmountOnClose={true}
|
||||
onClose={this.onClose}
|
||||
>
|
||||
{{
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { InjectionKey } from "vue";
|
||||
import { FormItems } from "./useFormItems";
|
||||
import { FormModel } from "./useFormModel";
|
||||
import { FormRef } from "./useFormRef";
|
||||
import { FormSubmit } from "./useFormSubmit";
|
||||
|
||||
export type FormContextInterface = FormModel &
|
||||
FormItems &
|
||||
FormRef &
|
||||
FormSubmit & {
|
||||
slots: Recordable;
|
||||
};
|
||||
|
||||
export const FormContextKey = Symbol("FormContextKey") as InjectionKey<FormContextInterface>;
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { Ref } from 'vue';
|
||||
import { AnFormItemProps } from './FormItem';
|
||||
import { setterMap } from './FormSetter';
|
||||
|
||||
export function useFormItems(items: Ref<AnFormItemProps[]>, model: Ref<Recordable>) {
|
||||
const getItem = (field: string) => {
|
||||
return items.value.find(i => i.field === field);
|
||||
};
|
||||
|
||||
const getItemOptions = (field: string) => {
|
||||
const item = getItem(field);
|
||||
if (item) {
|
||||
return (item.setterProps as any)?.options;
|
||||
}
|
||||
};
|
||||
|
||||
const initItemOptions = (field: string) => {
|
||||
const item = getItem(field);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const setter = setterMap[item.setter!];
|
||||
if (!setter) {
|
||||
return;
|
||||
}
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
};
|
||||
|
||||
const initItems = () => {
|
||||
for (const item of items.value) {
|
||||
const setter = setterMap[item?.setter!];
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
}
|
||||
};
|
||||
|
||||
const initItem = (field: string) => {
|
||||
const item = getItem(field);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const setter = setterMap[item?.setter!];
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initItems();
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
getItem,
|
||||
initItem,
|
||||
initItems,
|
||||
getItemOptions,
|
||||
initItemOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export type FormItems = ReturnType<typeof useFormItems>;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 表单数据管理
|
||||
* @param initial 初始值
|
||||
* @returns
|
||||
*/
|
||||
export function useFormModel(model: Ref<Recordable>, clearValidate: any) {
|
||||
const initial = cloneDeep(model.value);
|
||||
|
||||
const resetModel = () => {
|
||||
model.value = cloneDeep(initial);
|
||||
clearValidate();
|
||||
};
|
||||
|
||||
const getInitialModel = () => {
|
||||
return initial;
|
||||
};
|
||||
|
||||
const setModel = (data: Recordable) => {
|
||||
for (const key of Object.keys(model.value)) {
|
||||
model.value[key] = data[key];
|
||||
}
|
||||
};
|
||||
|
||||
const getModel = () => {
|
||||
return formatModel(model.value);
|
||||
};
|
||||
|
||||
return {
|
||||
model,
|
||||
getInitialModel,
|
||||
resetModel,
|
||||
setModel,
|
||||
getModel,
|
||||
};
|
||||
}
|
||||
|
||||
export type FormModel = ReturnType<typeof useFormModel>;
|
||||
|
||||
export function formatModel(model: Recordable) {
|
||||
const data: Recordable = {};
|
||||
|
||||
for (const [key, value] of Object.entries(model)) {
|
||||
if (value === '') {
|
||||
continue;
|
||||
}
|
||||
if (/^\[.+\]$/.test(key)) {
|
||||
formatModelArray(key, value, data);
|
||||
continue;
|
||||
}
|
||||
if (/^\{.+\}$/.test(key)) {
|
||||
formatModelObject(key, value, data);
|
||||
continue;
|
||||
}
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function formatModelArray(key: string, value: any, data: Recordable) {
|
||||
let field = key.replaceAll(/\s/g, '');
|
||||
field = field.match(/^\[(.+)\]$/)?.[1] ?? '';
|
||||
|
||||
if (!field) {
|
||||
data[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
field.split(',').forEach((key, index) => {
|
||||
data[key] = value?.[index];
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function formatModelObject(key: string, value: any, data: Recordable) {
|
||||
let field = key.replaceAll(/\s/g, '');
|
||||
field = field.match(/^\{(.+)\}$/)?.[1] ?? '';
|
||||
|
||||
if (!field) {
|
||||
data[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of field.split(',')) {
|
||||
data[key] = value?.[key];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import { Message } from '@arco-design/web-vue';
|
||||
import { AnFormProps } from './Form';
|
||||
import { AnFormItemProps } from './FormItem';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
const SUBMIT_ITEM = {
|
||||
field: 'id',
|
||||
setter: 'submit' as const,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function useFormSubmit(props: AnFormProps, validate: any, getModel: any) {
|
||||
const loading = ref(false);
|
||||
const submitItem = ref<AnFormItemProps | null>(null);
|
||||
|
||||
if (props.submit) {
|
||||
submitItem.value = cloneDeep(SUBMIT_ITEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置loading
|
||||
* @param value 值
|
||||
*/
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const submitForm = async () => {
|
||||
if (await validate()) {
|
||||
return;
|
||||
}
|
||||
const submit = typeof props.submit === 'string' ? () => null : props.submit;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = getModel();
|
||||
const res = await submit?.(data, props.items ?? []);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(`提示: ${msg}`);
|
||||
} catch {
|
||||
console.log();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消提交
|
||||
*/
|
||||
const cancelForm = () => {};
|
||||
|
||||
return {
|
||||
loading,
|
||||
submitItem,
|
||||
setLoading,
|
||||
submitForm,
|
||||
cancelForm,
|
||||
};
|
||||
}
|
||||
|
||||
export type FormSubmit = ReturnType<typeof useFormSubmit>;
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>, emit?: any, model?: Ref<Recordable>) {
|
||||
const loading = ref(false);
|
||||
const origin = cloneDeep(props.model);
|
||||
|
||||
const submitForm = async () => {
|
||||
if (await formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = formRef.value?.getModel() ?? {};
|
||||
const res = await props.submit?.(data, props.items);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(msg);
|
||||
emit('submited', res);
|
||||
visible.value = false;
|
||||
if (model) {
|
||||
model.value = cloneDeep(origin);
|
||||
}
|
||||
} catch {
|
||||
// todo
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
setLoading,
|
||||
submitForm,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { Button } from '@arco-design/web-vue';
|
||||
|
||||
export function useModalTrigger(props: any, open: () => void) {
|
||||
const modalTrigger = () => {
|
||||
if (!props.trigger) {
|
||||
return null;
|
||||
}
|
||||
if (typeof props.trigger === 'function') {
|
||||
return <props.trigger model={props.model} items={props.items} open={open}></props.trigger>;
|
||||
}
|
||||
const internal = {
|
||||
text: '新增',
|
||||
buttonProps: {},
|
||||
buttonSlots: {},
|
||||
};
|
||||
if (typeof props.trigger === 'string') {
|
||||
internal.text = props.trigger;
|
||||
}
|
||||
if (typeof props.trigger === 'object') {
|
||||
Object.assign(internal, props.trigger);
|
||||
}
|
||||
return (
|
||||
<Button type="primary" {...internal.buttonProps} onClick={open}>
|
||||
{{
|
||||
...internal.buttonSlots,
|
||||
icon: () => <i class="icon-park-outline-add"></i>,
|
||||
default: () => internal.text,
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
return modalTrigger;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { merge } from 'lodash-es';
|
||||
import { AnForm, AnFormInstance, AnFormProps } from '../components/Form';
|
||||
import { FormItem, useItems } from './useItems';
|
||||
import { FormItem, useFormItems } from './useFormItems';
|
||||
|
||||
export type FormUseOptions = Partial<Omit<AnFormProps, 'items'>> & {
|
||||
/**
|
||||
|
|
@ -20,7 +20,7 @@ export type FormUseOptions = Partial<Omit<AnFormProps, 'items'>> & {
|
|||
export function useFormProps(options: FormUseOptions): Required<AnFormProps> {
|
||||
const { model: _model = {}, items: _items = [], submit = () => null, formProps = {} } = options;
|
||||
const model = merge({ id: undefined }, _model);
|
||||
const items = useItems(_items ?? [], model);
|
||||
const items = useFormItems(_items ?? [], model);
|
||||
return {
|
||||
model,
|
||||
items,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defaultsDeep, merge, omit } from 'lodash-es';
|
||||
import { defaultsDeep, has, merge, omit } from 'lodash-es';
|
||||
import { AnFormItemProps, AnFormItemPropsBase } from '../components/FormItem';
|
||||
import { SetterItem, setterMap } from '../components/FormSetter';
|
||||
import { Rule, useRules } from './useRules';
|
||||
import { Rule, useFormRules } from './useFormRules';
|
||||
|
||||
/**
|
||||
* 表单项数据
|
||||
|
|
@ -34,17 +34,26 @@ export type FormItem = Omit<AnFormItemPropsBase, 'rules'> &
|
|||
* ```
|
||||
*/
|
||||
rules?: Rule[];
|
||||
|
||||
/**
|
||||
* 参数 `setterProps.placeholder` 的快捷语法
|
||||
* @example
|
||||
* ```ts
|
||||
* '请输入用户名称'
|
||||
* ```
|
||||
*/
|
||||
placeholder?: string | string[];
|
||||
};
|
||||
|
||||
const ITEM: Partial<FormItem> = {
|
||||
setter: 'input',
|
||||
};
|
||||
|
||||
export function useItems(list: FormItem[], model: Recordable) {
|
||||
const items: AnFormItemProps[] = [];
|
||||
export function useFormItems(items: FormItem[], model: Recordable) {
|
||||
const data: AnFormItemProps[] = [];
|
||||
|
||||
for (const item of list) {
|
||||
let target: any = defaultsDeep({}, ITEM);
|
||||
for (const item of items) {
|
||||
let target: AnFormItemProps = defaultsDeep({}, ITEM);
|
||||
|
||||
if (!item.setter || typeof item.setter === 'string') {
|
||||
const setter = setterMap[item.setter ?? 'input'];
|
||||
|
|
@ -53,16 +62,23 @@ export function useItems(list: FormItem[], model: Recordable) {
|
|||
}
|
||||
}
|
||||
|
||||
target = merge(target, omit(item, ['required', 'rules', 'value']));
|
||||
target = merge(target, omit(item, ['required', 'rules', 'value', 'placeholder']));
|
||||
|
||||
const rules = useRules(item);
|
||||
if (rules) {
|
||||
if (item.required || item.rules) {
|
||||
const rules = useFormRules(item)!;
|
||||
target.rules = rules;
|
||||
}
|
||||
|
||||
model[item.field] = model[item.field] ?? item.value;
|
||||
items.push(target);
|
||||
if (target.setterProps && has(item, 'placeholder')) {
|
||||
(target.setterProps as Recordable).placholder = item.placeholder;
|
||||
}
|
||||
|
||||
if (has(item, 'value')) {
|
||||
model[item.field] = item.value;
|
||||
}
|
||||
|
||||
data.push(target);
|
||||
}
|
||||
|
||||
return items;
|
||||
return data;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { merge } from 'lodash-es';
|
||||
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
|
||||
import { useFormProps } from './useForm';
|
||||
import { FormItem } from './useItems';
|
||||
import { FormItem } from './useFormItems';
|
||||
|
||||
export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
|
||||
/**
|
||||
|
|
@ -13,6 +13,10 @@ export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
|
|||
* ```
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* modal宽度
|
||||
*/
|
||||
modalWidth?: number;
|
||||
/**
|
||||
* 表单类名
|
||||
* @description 参数 `formProps.class` 的便捷语法
|
||||
|
|
@ -58,7 +62,7 @@ export function useFormModalProps(options: FormModalUseOptions): AnFormModalProp
|
|||
|
||||
export function useFormModal(options: FormModalUseOptions) {
|
||||
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
|
||||
const formRef = computed(() => modalRef.value?.formRef);
|
||||
const formRef = computed(() => modalRef.value?.anFormRef);
|
||||
const open = (data: Recordable = {}) => modalRef.value?.open(data);
|
||||
const rawProps = useFormModalProps(options);
|
||||
const props = reactive(rawProps);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
|
|||
* @param item 表单项
|
||||
* @returns
|
||||
*/
|
||||
export const useRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => {
|
||||
export const useFormRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => {
|
||||
const data: AnFormItemRule[] = [];
|
||||
const { required, rules } = item;
|
||||
|
||||
|
|
@ -2,15 +2,11 @@ export * from './components/Form';
|
|||
export * from './components/FormItem';
|
||||
export * from './components/FormModal';
|
||||
export * from './components/FormSetter';
|
||||
export * from './components/useFormContext';
|
||||
export * from './components/useFormItems';
|
||||
export * from './components/useFormModel';
|
||||
export * from './components/useFormRef';
|
||||
export * from './components/useFormSubmit';
|
||||
export * from './components/useModalSubmit';
|
||||
export * from './components/useModalTrigger';
|
||||
export * from './utils/useFormItems';
|
||||
export * from './utils/useFormModel';
|
||||
export * from './utils/useFormRef';
|
||||
export * from './hooks/useForm';
|
||||
export * from './hooks/useFormModal';
|
||||
export * from './hooks/useItems';
|
||||
export * from './hooks/useRules';
|
||||
export * from './hooks/useFormItems';
|
||||
export * from './hooks/useFormRules';
|
||||
export * from './setters';
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import { Button } from '@arco-design/web-vue';
|
||||
import { FormContextKey } from '../components/useFormContext';
|
||||
import { FormContextKey } from '../components/Form';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
export default defineSetter<{}, 'none'>({
|
||||
setter() {
|
||||
const { loading, submitForm, resetModel } = inject(FormContextKey)!;
|
||||
const { submitForm, resetForm } = inject(FormContextKey)!;
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" loading={loading.value} onClick={submitForm} class="mr-3">
|
||||
<Button type="primary" onClick={submitForm} class="mr-3">
|
||||
提交
|
||||
</Button>
|
||||
<Button disabled={loading.value} onClick={resetModel}>
|
||||
重置
|
||||
</Button>
|
||||
<Button onClick={resetForm}>重置</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { AnFormItemProps } from '../components/FormItem';
|
||||
import { setterMap } from '../components/FormSetter';
|
||||
|
||||
export const getFormItem = (items: AnFormItemProps[], field: string) => {
|
||||
return items.find(i => i.field === field);
|
||||
};
|
||||
|
||||
export const initFormItems = (items: AnFormItemProps[], model: Recordable) => {
|
||||
for (const item of items) {
|
||||
const setter = setterMap[item.setter!];
|
||||
setter.onSetup?.({ item, items, model });
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
export function getModel(model: Recordable) {
|
||||
const data: Recordable = {};
|
||||
|
||||
for (const [key, value] of Object.entries(model)) {
|
||||
if (value === '') {
|
||||
continue;
|
||||
}
|
||||
if (/^\[.+\]$/.test(key)) {
|
||||
getModelArray(key, value, data);
|
||||
continue;
|
||||
}
|
||||
if (/^\{.+\}$/.test(key)) {
|
||||
getModelObject(key, value, data);
|
||||
continue;
|
||||
}
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function setModel(model: Recordable, data: Recordable) {
|
||||
for (const [key, value] of Object.entries(model)) {
|
||||
if (/^\[.+\]$/.test(key)) {
|
||||
model[key] = setModelArray(data, key);
|
||||
continue;
|
||||
}
|
||||
if (/^\{.+\}$/.test(key)) {
|
||||
model[key] = setModelObject(data, key);
|
||||
continue;
|
||||
}
|
||||
model[key] = data[key];
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
function rmString(str: string) {
|
||||
const field = str.replaceAll(/\s/g, '');
|
||||
return field.match(/^(\{|\[)(.+)(\}|\])$/)?.[1] ?? '';
|
||||
}
|
||||
|
||||
function setModelArray(data: Recordable, key: string) {
|
||||
const result = [];
|
||||
const field = rmString(key);
|
||||
for (const key of field.split(',')) {
|
||||
result.push(data[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setModelObject(data: Recordable, key: string) {
|
||||
const result: Recordable = {};
|
||||
const field = rmString(key);
|
||||
for (const key of field.split(',')) {
|
||||
result[key] = data[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getModelArray(key: string, value: any, data: Recordable) {
|
||||
let field = rmString(key);
|
||||
|
||||
if (!field) {
|
||||
data[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
field.split(',').forEach((key, index) => {
|
||||
data[key] = value?.[index];
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getModelObject(key: string, value: any, data: Recordable) {
|
||||
const field = rmString(key);
|
||||
|
||||
if (!field) {
|
||||
data[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of field.split(',')) {
|
||||
data[key] = value?.[key];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@ import {
|
|||
AnFormModalInstance,
|
||||
AnFormModalProps,
|
||||
AnFormProps,
|
||||
getModel,
|
||||
} from '@/components/AnForm';
|
||||
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
||||
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
||||
import { isArray, isFunction, merge } from 'lodash-es';
|
||||
import { InjectionKey, PropType, Ref, defineComponent, ref } from 'vue';
|
||||
import { InjectionKey, PropType, Ref, VNodeChild, defineComponent, ref } from 'vue';
|
||||
import { PluginContainer } from '../hooks/useTablePlugin';
|
||||
|
||||
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
|
||||
|
|
@ -21,6 +22,8 @@ export type ArcoTableProps = Omit<
|
|||
|
||||
export const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey<AnTableContext>;
|
||||
|
||||
export type TableColumnRender = (data: { record: TableData; column: TableColumnData; rowIndex: number }) => VNodeChild;
|
||||
|
||||
/**
|
||||
* 表格组件
|
||||
*/
|
||||
|
|
@ -119,7 +122,7 @@ export const AnTable = defineComponent({
|
|||
}
|
||||
|
||||
const paging = getPaging();
|
||||
const search = searchRef.value?.getModel() ?? {};
|
||||
const search = getModel(props.search?.model ?? {});
|
||||
|
||||
if (isArray(props.source)) {
|
||||
// todo
|
||||
|
|
@ -129,14 +132,20 @@ export const AnTable = defineComponent({
|
|||
try {
|
||||
loading.value = true;
|
||||
let params = { ...search, ...paging };
|
||||
params = props.pluginer?.callLoadHook(params) ?? params;
|
||||
let resData = await props.source(params);
|
||||
resData = props.pluginer?.callLoadedHook(resData) ?? params;
|
||||
const { data = [], total = 0 } = resData?.data || {};
|
||||
let resData = (await props.pluginer?.callLoadHook(props.source, params)) || (await props.source(params));
|
||||
let data: any[] = [];
|
||||
let total = 0;
|
||||
if (isArray(resData)) {
|
||||
data = resData;
|
||||
total = resData.length;
|
||||
} else {
|
||||
data = resData.data.data;
|
||||
total = resData.data.total;
|
||||
}
|
||||
renderData.value = data;
|
||||
setPaging({ total });
|
||||
} catch (e) {
|
||||
// todo
|
||||
console.log('AnTable load fail: ', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { defaultsDeep, isArray, merge } from 'lodash-es';
|
||||
import { AnFormProps, FormUseOptions, AnFormItemProps, FormItem, useItems } from '@/components/AnForm';
|
||||
import { AnFormProps, FormUseOptions, AnFormItemProps, FormItem, useFormItems } from '@/components/AnForm';
|
||||
|
||||
export type ExtendFormItem = Partial<
|
||||
FormItem & {
|
||||
|
|
@ -14,9 +14,10 @@ export type ExtendFormItem = Partial<
|
|||
}
|
||||
>;
|
||||
|
||||
type SearchFormItem = ExtendFormItem & {
|
||||
export type SearchFormItem = ExtendFormItem & {
|
||||
/**
|
||||
* 是否点击图标后进行搜索
|
||||
* @description 仅 setter: 'search' 类型可用
|
||||
* @default
|
||||
* ```ts
|
||||
* false
|
||||
|
|
@ -33,7 +34,7 @@ type SearchFormItem = ExtendFormItem & {
|
|||
enterable?: boolean;
|
||||
};
|
||||
|
||||
export type SearchFormObject = Omit<FormUseOptions, 'items' | 'submit'> & {
|
||||
export type SearchForm = Omit<FormUseOptions, 'items' | 'submit'> & {
|
||||
/**
|
||||
* 搜索表单项
|
||||
* @example
|
||||
|
|
@ -54,9 +55,10 @@ export type SearchFormObject = Omit<FormUseOptions, 'items' | 'submit'> & {
|
|||
hideSearch?: boolean;
|
||||
};
|
||||
|
||||
export type SearchForm = SearchFormObject | SearchFormItem[];
|
||||
|
||||
export function useSearchForm(search?: SearchForm, extendItems: AnFormItemProps[] = []): AnFormProps | undefined {
|
||||
export function useSearchForm(
|
||||
search?: SearchForm | SearchFormItem[],
|
||||
extendItems: AnFormItemProps[] = []
|
||||
): AnFormProps | undefined {
|
||||
if (!search) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -95,7 +97,7 @@ export function useSearchForm(search?: SearchForm, extendItems: AnFormItemProps[
|
|||
item = merge({}, extendItem, itemRest);
|
||||
}
|
||||
}
|
||||
if (searchable) {
|
||||
if (searchable && item.setter === 'search') {
|
||||
(item as any).setterProps.onSearch = () => null;
|
||||
}
|
||||
if (enterable) {
|
||||
|
|
@ -107,7 +109,7 @@ export function useSearchForm(search?: SearchForm, extendItems: AnFormItemProps[
|
|||
items.push(item);
|
||||
}
|
||||
|
||||
props.items = useItems(items, props.model);
|
||||
props.items = useFormItems(items, props.model);
|
||||
|
||||
return props;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useFormModalProps } from '@/components/AnForm';
|
||||
import { AnTable, AnTableInstance, AnTableProps } from '../components/Table';
|
||||
import { ModifyForm, useModifyForm } from './useModiyForm';
|
||||
import { SearchForm, useSearchForm } from './useSearchForm';
|
||||
import { SearchForm, SearchFormItem, useSearchForm } from './useSearchForm';
|
||||
import { TableColumn, useTableColumns } from './useTableColumn';
|
||||
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
||||
import { UseCreateFormOptions } from './useCreateForm';
|
||||
|
|
@ -46,7 +46,7 @@ export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tablePro
|
|||
* }]
|
||||
* ```
|
||||
*/
|
||||
search?: SearchForm;
|
||||
search?: SearchForm | SearchFormItem[];
|
||||
/**
|
||||
* 新建弹窗
|
||||
* @example
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ interface TableColumnButton {
|
|||
* @see ALink
|
||||
*/
|
||||
buttonProps?: Recordable;
|
||||
icon?: string;
|
||||
/**
|
||||
* 是否可见
|
||||
* @example
|
||||
|
|
@ -155,7 +156,10 @@ function useTableButtonColumn(column: TableButtonColumn & TableColumnData) {
|
|||
<>
|
||||
{index !== 0 && <Divider direction="vertical" margin={2} />}
|
||||
<Link {...item.buttonProps} disabled={item.disable?.(props)} onClick={() => item.onClick?.(props)}>
|
||||
{item.text}
|
||||
{{
|
||||
default: () => item.text,
|
||||
// icon: () => item.icon ? <i class={item.icon}></i> : null
|
||||
}}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,17 +28,25 @@ export interface AnTablePlugin {
|
|||
provide?: Recordable;
|
||||
|
||||
/**
|
||||
* 组件钩子
|
||||
* @description 对应表格组件的 `setup` 钩子
|
||||
* 在表格组件的 `setup` 函数中调用
|
||||
*/
|
||||
onSetup?: (context: AnTableContext) => void;
|
||||
|
||||
/**
|
||||
* 钩子
|
||||
* @description 在处理前进行参数处理
|
||||
*/
|
||||
options?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
|
||||
|
||||
/**
|
||||
* 解析参数之前调用
|
||||
*/
|
||||
parse?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
|
||||
|
||||
/**
|
||||
* 解析参数之后调用
|
||||
*/
|
||||
parsed?: (options: any) => any;
|
||||
|
||||
/**
|
||||
* 表格列
|
||||
*/
|
||||
|
|
@ -62,15 +70,31 @@ export interface AnTablePlugin {
|
|||
*/
|
||||
action?: () => (props: any) => any | Component;
|
||||
|
||||
/**
|
||||
* 搜索前处理
|
||||
*
|
||||
*/
|
||||
onBeforeSearch?: (args: { page: number; size: number; [key: string]: any }) => Recordable | null | undefined | void;
|
||||
onSearch?: (search: Recordable) => any[] | { data: any[]; total: number };
|
||||
|
||||
onLoad?: (search: Recordable) => void;
|
||||
onLoaded?: (res: any) => void;
|
||||
onLoadOk?: (res: any) => void;
|
||||
onLoadFail?: (e: any) => void;
|
||||
}
|
||||
|
||||
const callHookWithData = async (name: string, plugins: AnTablePlugin[], data?: any) => {
|
||||
for (const plugin of plugins) {
|
||||
data = (await (plugin as any)[name]?.(data)) ?? data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const callHookFirst = async (name: string, plugins: AnTablePlugin[], ...args: any[]) => {
|
||||
for (const plugin of plugins) {
|
||||
const data = await (plugin as any)[name]?.(...args);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export class PluginContainer {
|
||||
actions: any[] = [];
|
||||
widgets: any[] = [];
|
||||
|
|
@ -116,24 +140,19 @@ export class PluginContainer {
|
|||
return options;
|
||||
}
|
||||
|
||||
callBeforeSearchHook(options: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
options = plugin.onBeforeSearch?.(options) ?? options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
callLoadHook(search: Recordable) {
|
||||
for (const plugin of this.plugins) {
|
||||
search = plugin.onLoad?.(search) ?? search;
|
||||
}
|
||||
return search as any;
|
||||
callLoadHook(data: any[] | ((...args: any[]) => Promise<any> | any), params: Recordable) {
|
||||
return callHookFirst('onLoad', this.plugins, data, params);
|
||||
}
|
||||
|
||||
callLoadedHook(res: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
res = plugin.onLoaded?.(res) ?? res;
|
||||
}
|
||||
return res;
|
||||
return callHookWithData('onLoaded', this.plugins, res);
|
||||
}
|
||||
|
||||
callLoadOkHook(res: any) {
|
||||
return callHookWithData('onLoadOk', this.plugins, res);
|
||||
}
|
||||
|
||||
callLoadFailHook(res: any) {
|
||||
return callHookWithData('onLoadFail', this.plugins, res);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export function useRowModify(): AnTablePlugin {
|
|||
}
|
||||
const onClick = btn.onClick;
|
||||
btn.onClick = async props => {
|
||||
const { modifyRef } = ctx ?? {};
|
||||
modifyRef?.value?.open(props.record);
|
||||
const data = (await onClick?.(props)) ?? props.record;
|
||||
ctx.modifyRef.value?.open(data);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Blocker } from "../core";
|
||||
import { InjectionKey } from 'vue';
|
||||
import { Block, Blocker, Container } from '../core';
|
||||
import { useTextBlock } from './text';
|
||||
|
||||
const blockers: Record<string, Blocker> = import.meta.glob(["./*/index.ts", "!./font/*"], {
|
||||
const blockers: Record<string, Blocker> = import.meta.glob(['./*/index.ts', '!./font/*'], {
|
||||
eager: true,
|
||||
import: "default",
|
||||
import: 'default',
|
||||
});
|
||||
const BlockerMap: Record<string, Blocker> = {};
|
||||
|
||||
|
|
@ -23,3 +25,47 @@ const getIcon = (type: string) => {
|
|||
};
|
||||
|
||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
||||
|
||||
export const BlockerManagerKey = Symbol('k') as InjectionKey<ReturnType<typeof useBlockerManage>>
|
||||
|
||||
export function useBlockerManage() {
|
||||
const blockers: Blocker[] = [useTextBlock()];
|
||||
const leftPanels: any[] = [];
|
||||
|
||||
for (const blocker of blockers) {
|
||||
const panel = blocker.addLeftTab?.();
|
||||
if (panel) {
|
||||
leftPanels.push(leftPanels);
|
||||
}
|
||||
}
|
||||
|
||||
const callInitHook = (container: Container) => {
|
||||
for (const blocker of blockers) {
|
||||
container = blocker.onLoadContainer?.(container) || container;
|
||||
}
|
||||
return container;
|
||||
};
|
||||
|
||||
const callLoadHook = (data: any): Blocker => {
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onLoadBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const callSaveHook = (block: Block) => {
|
||||
let data = block;
|
||||
for (const blocker of blockers) {
|
||||
data = blocker.onSaveBlock?.(data) || data;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return {
|
||||
blockers,
|
||||
leftPanels,
|
||||
callInitHook,
|
||||
callLoadHook,
|
||||
callSaveHook,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import { defineBlocker } from "../../core";
|
||||
import { font } from "../font";
|
||||
import { Text } from "./interface";
|
||||
import Option from "./option.vue";
|
||||
import Render from "./render.vue";
|
||||
import { Block, Blocker, defineBlocker } from '../../core';
|
||||
import { font } from '../font';
|
||||
import { Text } from './interface';
|
||||
import Option from './option.vue';
|
||||
import Render from './render.vue';
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: "text",
|
||||
icon: "icon-park-outline-text",
|
||||
title: "文本组件",
|
||||
description: "文字",
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: {
|
||||
id: "",
|
||||
type: "text",
|
||||
title: "",
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: "",
|
||||
bgColor: "",
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
|
|
@ -30,12 +30,58 @@ export default defineBlocker<Text>({
|
|||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: "left",
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
"温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。",
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useTextBlock(): Blocker<Text> {
|
||||
const initialData: Text = {
|
||||
id: '',
|
||||
type: 'text',
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
h: 100,
|
||||
xFixed: false,
|
||||
yFixed: false,
|
||||
bgImage: '',
|
||||
bgColor: '',
|
||||
meta: {},
|
||||
actived: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
params: {
|
||||
marquee: false,
|
||||
speed: 100,
|
||||
direction: 'left',
|
||||
fontCh: {
|
||||
...font,
|
||||
content:
|
||||
'温馨提示:乘客您好,进站检票时,持票卡的乘客请在右侧闸机上方感应区内验票,扫码过闸的乘客请将乘车码对准闸机扫码口,扇门打开后依次进闸。乘车过程中请妥善保管好车票,以免丢失。',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
icon: 'icon-park-outline-text',
|
||||
title: '文本组件',
|
||||
description: '文字',
|
||||
render: Render,
|
||||
option: Option,
|
||||
initial: initialData,
|
||||
addLeftTab() {
|
||||
return {
|
||||
title: '文本测试',
|
||||
icon: 'icon-park-outline-user',
|
||||
component: () => h('div', null, 'TODO')
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { Block } from "../../core";
|
||||
import { Font } from "../font";
|
||||
|
||||
export interface OutputText {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface TextPrams {
|
||||
/**
|
||||
* 是否滚动
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@
|
|||
</template>
|
||||
预览
|
||||
</a-button>
|
||||
<a-button @click="emit('config')">
|
||||
<!-- <a-button @click="emit('config')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config"></i>
|
||||
</template>
|
||||
设置
|
||||
</a-button>
|
||||
</a-button> -->
|
||||
<a-button type="primary" :loading="saving" @click="emit('save')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-save"></i>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${collapsed ? 'none' : 'block'}`">
|
||||
<div v-if="model" class="p-3 pr-0 grid grid-rows-[auto_1fr]">
|
||||
<a-tag class="text-sm! mb-2 mr-3" size="large" color="blue" :bordered="true">
|
||||
<a-tag class="text-sm! mb-2 mr-3" size="large" color="blue" :bordered="false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-components"></i>
|
||||
<i :class="BlockerMap[model.type].icon"></i>
|
||||
</template>
|
||||
组件属性({{ BlockerMap[model.type].title }})
|
||||
{{ BlockerMap[model.type].title }}属性
|
||||
</a-tag>
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<a-form :model="{}" layout="vertical" class="pr-3">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="p-3">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="blue" :bordered="true">
|
||||
<a-tag class="text-sm! mb-2 w-full" size="large" color="blue" :bordered="false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config" ></i>
|
||||
</template>
|
||||
画布设置
|
||||
画布属性
|
||||
</a-tag>
|
||||
<a-form :model="{}" layout="vertical">
|
||||
<a-form-item label="标题">
|
||||
|
|
@ -31,6 +31,22 @@
|
|||
<a-form-item label="背景颜色">
|
||||
<input-color v-model="model.bgColor"></input-color>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="语言列表">
|
||||
<a-checkbox-group v-model="model.langList" direction="vertical" class="bg-gray-100 w-full px-1.5 py-1 rounded">
|
||||
<a-checkbox value="ch">中文<span class="text-gray-400">(cn)</span></a-checkbox>
|
||||
<a-checkbox value="en">英语<span class="text-gray-400">(en)</span></a-checkbox>
|
||||
<a-checkbox value="ru">俄语<span class="text-gray-400">(ru)</span></a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="语言切换">
|
||||
<a-input-number v-model="model.langSwitch" :min="0">
|
||||
<template #append>
|
||||
秒(s)
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Component } from "vue";
|
||||
import { Block } from "./block";
|
||||
import { Component } from 'vue';
|
||||
import { Block } from './block';
|
||||
import { Container } from './container';
|
||||
|
||||
/**
|
||||
* 组件配置
|
||||
|
|
@ -38,11 +39,63 @@ export interface Blocker<T = any> {
|
|||
*/
|
||||
viewer?: Component;
|
||||
/**
|
||||
* 初始化钩子
|
||||
* @param block 组件
|
||||
* @returns
|
||||
* 将实际格式转换为内部格式
|
||||
*/
|
||||
onInit?: (block: Block) => void;
|
||||
onLoadContainer?: (container: Container) => void;
|
||||
/**
|
||||
* 将内部格式转换为实际格式
|
||||
*/
|
||||
onSaveContainer?: (container: Container) => any;
|
||||
/**
|
||||
* 将实际格式转换为内部格式
|
||||
*/
|
||||
onLoadBlock?: (data: any) => Block;
|
||||
/**
|
||||
* 将内部格式转换为实际格式
|
||||
*/
|
||||
onSaveBlock?: (block: Block) => any;
|
||||
/**
|
||||
* 在左侧添加选项卡
|
||||
*/
|
||||
addLeftTab?: () => {
|
||||
title: string;
|
||||
icon: string | Component;
|
||||
component: Component;
|
||||
};
|
||||
addBlock?: () => {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 显示标题
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 显示图标
|
||||
*/
|
||||
icon: string;
|
||||
/**
|
||||
* 显示描述
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* 默认初始值
|
||||
*/
|
||||
initial: Block;
|
||||
/**
|
||||
* 预览时的渲染组件
|
||||
*/
|
||||
render: Component;
|
||||
/**
|
||||
* 编辑参数时的渲染组件
|
||||
*/
|
||||
optionRender: Component<{ modelValue: Block }>;
|
||||
/**
|
||||
* 编辑时的渲染组件
|
||||
*/
|
||||
modifyRender: Component;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export interface Container {
|
|||
* 背景颜色
|
||||
*/
|
||||
bgColor: string;
|
||||
langList: string[];
|
||||
langSwitch: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,4 +60,6 @@ export const defaultContainer: Container = {
|
|||
height: 1080,
|
||||
bgImage: "",
|
||||
bgColor: "#ffffff",
|
||||
langList: ['ch', 'en'],
|
||||
langSwitch: 0
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,10 +31,16 @@
|
|||
</a-doption>
|
||||
<a-doption @click="router.push('/my')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config"></i>
|
||||
<i class="icon-park-outline-user"></i>
|
||||
</template>
|
||||
个人设置
|
||||
</a-doption>
|
||||
<a-doption @click="router.push('/my')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config"></i>
|
||||
</template>
|
||||
系统设置
|
||||
</a-doption>
|
||||
<a-divider :margin="4"></a-divider>
|
||||
<a-doption @click="logout">
|
||||
<template #icon>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700"
|
||||
>
|
||||
<div class="h-13 flex items-center">
|
||||
<router-link to="/" class="px-2 py-1 rounded flex items-center gap-2 text-slate-700">
|
||||
<img src="/favicon.ico" alt="" width="22" height="22" class="" />
|
||||
<h1 class="relative text-lg leading-[20px] dark:text-white m-0 p-0 font-semibold">
|
||||
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
||||
<h1 class="relative text-[22px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||
{{ appStore.title }}
|
||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||
</h1>
|
||||
|
|
@ -16,6 +16,15 @@
|
|||
<div>
|
||||
<a-input-search placeholder="搜索菜单/页面" :allow-clear="true"></a-input-search>
|
||||
</div>
|
||||
<a-tooltip content="上传文件">
|
||||
<a-button @click="() => null" class="!bg-transparent !hover:bg-gray-100">
|
||||
<template #icon>
|
||||
<a-badge :count="1" :dot="true">
|
||||
<i class="text-base icon-park-outline-upload-one"></i>
|
||||
</a-badge>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
|
||||
<a-button @click="btn.onClick" class="!bg-transparent !hover:bg-gray-100">
|
||||
<template #icon>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"
|
||||
></div>
|
||||
<div class="relative p-20 px-8 md:px-14 bg-white shadow-sm">
|
||||
<div class="text-2xl">欢迎登陆</div>
|
||||
<div class="text-base text-gray-500 mt-2">{{ meridiem }}好,欢迎登陆{{ appStore.title }}!</div>
|
||||
<a-form ref="formRef" :model="model" :rules="formRules" layout="vertical" class="mt-8">
|
||||
<div class="text-xl text-brand-500 font-semibold">用户登陆</div>
|
||||
<div class="text-gray-500 mt-2.5">{{ meridiem }}好,欢迎访问 {{ appStore.title }} 系统!</div>
|
||||
<a-form ref="formRef" :model="model" :rules="formRules" layout="vertical" class="mt-6">
|
||||
<a-form-item field="username" label="账号" :disabled="loading" hide-asterisk>
|
||||
<a-input v-model="model.username" placeholder="请输入账号/手机号/邮箱" allow-clear>
|
||||
<template #prefix>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,19 @@
|
|||
</a-button>
|
||||
<CategoryModal></CategoryModal>
|
||||
</div>
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<a-spin :loading="loading" class="w-full h-full">
|
||||
<a-spin :loading="loading" class="w-full h-full">
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<ul v-if="list.length" class="pl-0 mt-0">
|
||||
<li
|
||||
:class="{ active: !current?.id }"
|
||||
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', {})">
|
||||
<i class="icon-park-outline-folder-close align-[-2px]"></i>
|
||||
<span class="flex-1 truncate">全部</span>
|
||||
</div>
|
||||
<div class=""></div>
|
||||
</li>
|
||||
<li
|
||||
v-for="item in list"
|
||||
:key="item.code"
|
||||
|
|
@ -48,8 +58,8 @@
|
|||
</li>
|
||||
</ul>
|
||||
<an-empty v-else></an-empty>
|
||||
</a-spin>
|
||||
</a-scrollbar>
|
||||
</a-scrollbar>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -75,7 +85,6 @@ const updateFileCategories = async () => {
|
|||
loading.value = true;
|
||||
const res = await api.fileCategory.getFileCategorys({ size: 0 });
|
||||
list.value = res.data.data ?? [];
|
||||
list.value.unshift({ id: undefined, name: '全部' } as any);
|
||||
list.value.length && emit('change', list.value[0]);
|
||||
} catch {
|
||||
// nothing to do
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<template v-if="fileType === 'image'">
|
||||
<a-image-preview v-model:visible="show" :src="url"></a-image-preview>
|
||||
</template>
|
||||
<template v-else-if="fileType === 'video'">
|
||||
<a-modal v-model:visible="show" title="预览" title-align="start" :footer="false">
|
||||
<video :src="url" controls></video>
|
||||
</a-modal>
|
||||
</template>
|
||||
<template v-else-if="fileType === 'text'">
|
||||
<a-modal v-model:visible="show"></a-modal>
|
||||
</template>
|
||||
<template v-else-if="fileType === 'audio'">
|
||||
<a-modal v-model:visible="show" :footer="false"></a-modal>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-modal v-model:visible="show" title="预览" title-align="start" :closable="false" :width="420">
|
||||
抱歉,此文件类型暂不支持预览!
|
||||
</a-modal>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
type FileType = 'text' | 'audio' | 'image' | 'video' | 'unknown';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<FileType>,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const fileType = computed<FileType>(() => {
|
||||
if (props.type === 'text') {
|
||||
return 'text';
|
||||
}
|
||||
if (props.type === 'image') {
|
||||
return 'image';
|
||||
}
|
||||
if (props.type === 'video') {
|
||||
return 'video';
|
||||
}
|
||||
if (props.type === 'audio') {
|
||||
return 'audio';
|
||||
}
|
||||
return 'unknown';
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible']);
|
||||
const show = useVModel(props, 'visible', emit);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -38,66 +38,68 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ul v-if="fileList.length" class="h-[424px] overflow-hidden p-0 m-0">
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto pr-[20px] divide-y">
|
||||
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-4 py-3">
|
||||
<div class="text-4xl rounded pr-0.5 flex justify-center">
|
||||
<i :class="getIcon(item.file?.type ?? 'video')"></i>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="h-8 truncate text-slate-900 flex justify-between items-center gap-2">
|
||||
<div>
|
||||
{{ item.name }}
|
||||
<span class="text-xs text-gray-400 ml-2">{{ numeral(item.file?.size).format('0 b') }}</span>
|
||||
<div class="h-[424px] border-t border-b border-zinc-100 mt-3">
|
||||
<ul v-if="fileList.length" class="overflow-hidden p-0 m-0">
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto pr-[20px] divide-y">
|
||||
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-4 py-3">
|
||||
<div class="text-4xl rounded pr-0.5 flex justify-center">
|
||||
<i :class="getIcon(item.file?.type ?? 'video')"></i>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="h-8 truncate text-slate-900 flex justify-between items-center gap-2">
|
||||
<div>
|
||||
{{ item.name }}
|
||||
<span class="text-xs text-gray-400 ml-2">{{ numeral(item.file?.size).format('0 b') }}</span>
|
||||
</div>
|
||||
<div v-show="item.status !== 'done'">
|
||||
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)">停止</a-link>
|
||||
<a-link v-show="item.status === 'error'" @click="retryItem(item)">重试</a-link>
|
||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">
|
||||
删除
|
||||
</a-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="item.status !== 'done'">
|
||||
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)">停止</a-link>
|
||||
<a-link v-show="item.status === 'error'" @click="retryItem(item)">重试</a-link>
|
||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">
|
||||
删除
|
||||
</a-link>
|
||||
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block!"></a-progress>
|
||||
<div class="flex items-center justify-between gap-2 text-gray-400 mt-1.5 text-xs">
|
||||
<span class="text-xs">
|
||||
<span v-if="item.status === 'init'">
|
||||
<i class="icon-park-outline-hourglass-full"></i>
|
||||
等待上传
|
||||
</span>
|
||||
<span v-else-if="item.status === 'uploading'" class="text-[rgb(var(--primary-6))]">
|
||||
<i class="icon-park-outline-upload-one"></i>
|
||||
正在上传
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'" class="text-[rgb(var(--success-6))]">
|
||||
<i class="icon-park-outline-check"></i>
|
||||
上传成功
|
||||
</span>
|
||||
<span v-else="item.status === 'error'" class="text-red-500">
|
||||
<i class="icon-park-outline-close"></i>
|
||||
上传失败
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="item.status === 'init'"> </span>
|
||||
<span v-else-if="item.status === 'uploading'">
|
||||
速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} %
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'">
|
||||
耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s
|
||||
</span>
|
||||
<span v-else="item.status === 'error'"> 原因:{{ fileMap.get(item.uid)?.error }} </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block!"></a-progress>
|
||||
<div class="flex items-center justify-between gap-2 text-gray-400 mt-1.5 text-xs">
|
||||
<span class="text-xs">
|
||||
<span v-if="item.status === 'init'">
|
||||
<i class="icon-park-outline-hourglass-full"></i>
|
||||
等待上传
|
||||
</span>
|
||||
<span v-else-if="item.status === 'uploading'" class="text-[rgb(var(--primary-6))]">
|
||||
<i class="icon-park-outline-upload-one"></i>
|
||||
正在上传
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'" class="text-[rgb(var(--success-6))]">
|
||||
<i class="icon-park-outline-check"></i>
|
||||
上传成功
|
||||
</span>
|
||||
<span v-else="item.status === 'error'" class="text-red-500">
|
||||
<i class="icon-park-outline-close"></i>
|
||||
上传失败
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="item.status === 'init'"> </span>
|
||||
<span v-else-if="item.status === 'uploading'">
|
||||
速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} %
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'">
|
||||
耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s
|
||||
</span>
|
||||
<span v-else="item.status === 'error'"> 原因:{{ fileMap.get(item.uid)?.error }} </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a-scrollbar>
|
||||
</ul>
|
||||
|
||||
<div v-else class="h-[424px] flex items-center justify-center">
|
||||
<an-empty></an-empty>
|
||||
</li>
|
||||
</a-scrollbar>
|
||||
</ul>
|
||||
<div v-else class="h-full flex items-center justify-center">
|
||||
<an-empty></an-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between gap-2 items-center">
|
||||
<div class="text-gray-400">已上传 {{ stat.doneCount }}/{{ fileList.length }} 项</div>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
const typeIconMap: Record<string, string> = {
|
||||
video: 'icon-park-outline-video-file',
|
||||
audio: 'icon-park-outline-audio-file',
|
||||
image: 'icon-park-outline-file-pdf',
|
||||
text: 'icon-park-outline-file-txt',
|
||||
application: 'icon-park-outline-file-code',
|
||||
unknown: 'icon-park-outline-file-question',
|
||||
};
|
||||
|
||||
function getIconnameByMimetype(mimetype: string) {
|
||||
const [type, subtype] = mimetype.split('/');
|
||||
return typeIconMap[type] || typeIconMap.unknown;
|
||||
}
|
||||
|
||||
enum MIME {
|
||||
IMAGE = 'image',
|
||||
VIDEO = 'video',
|
||||
AUDIO = 'audio',
|
||||
TEXT = 'text',
|
||||
APPLICATION = 'application',
|
||||
}
|
||||
|
||||
function getIcon(mimetype: string) {
|
||||
const [type, subtype] = mimetype.split('/');
|
||||
if (type === MIME.IMAGE) {
|
||||
return 'icon-file-iimage';
|
||||
}
|
||||
if (type === MIME.VIDEO) {
|
||||
return 'icon-file-ivideo';
|
||||
}
|
||||
if (type === MIME.TEXT) {
|
||||
return 'icon-file-itxt';
|
||||
}
|
||||
if (type === MIME.AUDIO) {
|
||||
return 'icon-file-iaudio';
|
||||
}
|
||||
if (type === MIME.APPLICATION) {
|
||||
if (subtype === 'zip') {
|
||||
return 'icon-file-izip';
|
||||
}
|
||||
}
|
||||
return 'icon-file-iunknown';
|
||||
}
|
||||
|
||||
export { getIcon, getIconnameByMimetype };
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<AnUpload @success="() => tableRef?.refresh()"></AnUpload>
|
||||
</template>
|
||||
</MaterialTable>
|
||||
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
||||
<AnPreview v-model:visible="viewer.visible" :type="viewer.type" :url="viewer.url"></AnPreview>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -23,19 +23,17 @@ import { Message } from '@arco-design/web-vue';
|
|||
import numeral from 'numeral';
|
||||
import AnGroup from './components/AnGroup.vue';
|
||||
import AnUpload from './components/AnUpload.vue';
|
||||
import AnPreview from './components/AnPreview.vue';
|
||||
import { getIcon } from './components/util';
|
||||
|
||||
const visible = ref(false);
|
||||
const current = ref<FileCategory>();
|
||||
const image = ref('');
|
||||
const viewer = reactive({ visible: false, url: undefined, type: undefined });
|
||||
|
||||
const preview = (record: any) => {
|
||||
if (!record.mimetype.startsWith('image')) {
|
||||
window.open(record.path, '_blank');
|
||||
return;
|
||||
}
|
||||
image.value = record.path;
|
||||
visible.value = true;
|
||||
const [type] = record.mimetype.split('/');
|
||||
viewer.url = record.path;
|
||||
viewer.type = type;
|
||||
viewer.visible = true;
|
||||
};
|
||||
|
||||
const onCategoryChange = (category: FileCategory) => {
|
||||
|
|
@ -48,7 +46,7 @@ const onCategoryChange = (category: FileCategory) => {
|
|||
|
||||
const copyLink = (record: Recordable) => {
|
||||
window.navigator.clipboard.writeText(record.path);
|
||||
Message.success(`提示:已复制 ${record.name} 的地址!`);
|
||||
Message.success(`已复制 ${record.name} 的地址!`);
|
||||
};
|
||||
|
||||
const {
|
||||
|
|
@ -63,10 +61,10 @@ const {
|
|||
dataIndex: 'name',
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<div class="group flex items-center gap-2">
|
||||
<div class="group flex items-center gap-4">
|
||||
<div class="w-8 flex justify-center">
|
||||
{record.mimetype.startsWith('image') ? (
|
||||
<a-avatar size={26} shape="square">
|
||||
<a-avatar size={32} shape="square">
|
||||
<img src={record.path}></img>
|
||||
</a-avatar>
|
||||
) : (
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
const typeIconMap: Record<string, string> = {
|
||||
video: "icon-park-outline-video-file",
|
||||
audio: "icon-park-outline-audio-file",
|
||||
image: "icon-park-outline-file-pdf",
|
||||
text: "icon-park-outline-file-txt",
|
||||
application: "icon-park-outline-file-code",
|
||||
unknown: "icon-park-outline-file-question",
|
||||
};
|
||||
|
||||
function getIconnameByMimetype(mimetype: string) {
|
||||
const [type, subtype] = mimetype.split("/");
|
||||
return typeIconMap[type] || typeIconMap.unknown;
|
||||
}
|
||||
|
||||
function getIcon(mimetype: string) {
|
||||
if (mimetype.startsWith("image")) {
|
||||
return "icon-fmt-png";
|
||||
}
|
||||
if (mimetype.startsWith("video")) {
|
||||
return "icon-fmt-video";
|
||||
}
|
||||
if (mimetype.startsWith("text")) {
|
||||
return "icon-fmt-txt";
|
||||
}
|
||||
if (mimetype.startsWith("audio")) {
|
||||
return "icon-fmt-mp";
|
||||
}
|
||||
return "icon-fmt-visio";
|
||||
}
|
||||
|
||||
export { getIcon, getIconnameByMimetype };
|
||||
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
<template><div></div></template>
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<LoginLogTable>
|
||||
<template #action>
|
||||
<a-button type="primary" @click="visible = true">添加</a-button>
|
||||
<ani-editor v-model:visible="visible"></ani-editor>
|
||||
</template>
|
||||
</LoginLogTable>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { Editor as aniEditor } from '@/components/editor';
|
||||
import { TableColumnData } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineOptions({ name: 'SystemLoglPage' });
|
||||
const visible = ref(false);
|
||||
|
||||
const useTwoRowsColumn = (tkey: string, bkey: string): TableColumnData['render'] => {
|
||||
return ({ record }) => {
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record[tkey] || '未知'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">{record[bkey]}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const { component: LoginLogTable } = useTable({
|
||||
source: async model => {
|
||||
return api.log.getLoginLogs(model);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '操作描述',
|
||||
dataIndex: 'description',
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={
|
||||
record.status === null || record.status
|
||||
? 'text-base text-green-500 icon-park-outline-check-one mr-2'
|
||||
: 'text-base text-red-500 icon-park-outline-close-one mr-2'
|
||||
}
|
||||
></span>
|
||||
<div>
|
||||
<div>{record.nickname}</div>
|
||||
<div class="text-xs text-gray-400">{record.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登陆地址',
|
||||
dataIndex: 'ip',
|
||||
width: 200,
|
||||
render: useTwoRowsColumn('addr', 'ip'),
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
const [os, version] = record.os.split(' ');
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{os || '未知'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">{version}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
dataIndex: 'browser',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
const [browser, version] = record.browser.split(' ');
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{browser || '未知'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">v{version}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登陆时间',
|
||||
dataIndex: 'createAt',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{dayjs(record.createdAt).fromNow()}</span>
|
||||
<span class="text-gray-400 text-xs truncate">{dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
field: '[startDate, endDate]',
|
||||
label: '登陆账号',
|
||||
setter: 'dateRange',
|
||||
setterProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
showTime: true,
|
||||
timePickerProps: { defaultValue: ['23:59:59', '00:00:00'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
label: '登陆账号',
|
||||
setter: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"name": "SystemLoglPage",
|
||||
"sort": 10303,
|
||||
"title": "登陆日志",
|
||||
"icon": "icon-park-outline-log"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<OperationTable></OperationTable>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { Tag } from '@arco-design/web-vue';
|
||||
|
||||
defineOptions({ name: 'SystemLogoPage' });
|
||||
|
||||
const { component: OperationTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '登陆账号',
|
||||
dataIndex: 'nickname',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '操作描述',
|
||||
dataIndex: 'description',
|
||||
render: ({ record: { status, description } }) => {
|
||||
return (
|
||||
<span>
|
||||
<Tag color={status === null || status ? 'green' : 'red'} class="mr-2">
|
||||
{status === null || status ? '成功' : '失败'}
|
||||
</Tag>
|
||||
{description}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登陆地址',
|
||||
dataIndex: 'ip',
|
||||
width: 200,
|
||||
render: ({ record }) => `${record.addr || '未知'}(${record.ip})`,
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
dataIndex: 'browser',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '登陆时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 120,
|
||||
render: ({ record }) => dayjs(record.createdAt).fromNow(),
|
||||
},
|
||||
],
|
||||
source: model => {
|
||||
return api.log.getLoginLogs(model);
|
||||
},
|
||||
search: [
|
||||
{
|
||||
field: 'nickname',
|
||||
label: '登陆账号',
|
||||
setter: 'input',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"name": "SystemLogoPage",
|
||||
"sort": 10304,
|
||||
"title": "操作日志",
|
||||
"icon": "icon-park-outline-doc-detail"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
|
||||
defineOptions({ name: 'SystemUserPage' });
|
||||
|
|
@ -30,37 +30,43 @@ const { component: PasswordModal, open } = useFormModal({
|
|||
submit: model => api.user.setUser(model.id, model as any),
|
||||
});
|
||||
|
||||
const usernameRender: TableColumnRender = ({ record }) => (
|
||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||
<a-avatar size={32} class="!bg-brand-500">
|
||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||
</a-avatar>
|
||||
<div class="w-full flex-1 overflow-hidden">
|
||||
<div>
|
||||
<span>{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
|
||||
</div>
|
||||
<div class="w-full text-gray-400 space-x-4 text-xs">
|
||||
<span>
|
||||
<i class="icon-park-outline-mail mr-1 align-[-4px]"></i>
|
||||
contact@juetan.cn
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon-park-outline-phone-telephone mr-1"></i>
|
||||
1591234568
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const { component: UserTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '用户昵称',
|
||||
dataIndex: 'username',
|
||||
render: ({ record }) => (
|
||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||
<a-avatar size={32} class="!bg-brand-500">
|
||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||
</a-avatar>
|
||||
<div class="w-full flex-1 overflow-hidden">
|
||||
<div>
|
||||
<span>{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
|
||||
</div>
|
||||
<div class="w-full text-gray-400 space-x-4 text-xs">
|
||||
<span>
|
||||
<i class="icon-park-outline-mail mr-1 align-[-4px]"></i>
|
||||
contact@juetan.cn
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon-park-outline-phone-telephone mr-1"></i>
|
||||
1591234568
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
render: usernameRender,
|
||||
},
|
||||
{
|
||||
...useCreateColumn(),
|
||||
},
|
||||
{
|
||||
...useUpdateColumn(),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -112,17 +118,13 @@ const { component: UserTable } = useTable({
|
|||
label: '登录账号',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
setterProps: {
|
||||
placeholder: '英文字母+数组组成,5~10位',
|
||||
},
|
||||
placeholder: '英文字母+数组组成,5~10位',
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
label: '登陆密码',
|
||||
setter: 'input',
|
||||
setterProps: {
|
||||
placeholder: '包含大小写,长度6 ~ 12位',
|
||||
},
|
||||
placeholder: '包含大小写,长度6 ~ 12位',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function useAuthGuard(router: Router) {
|
|||
};
|
||||
|
||||
router.beforeEach(async function (to, from) {
|
||||
console.log(to);
|
||||
const userStore = useUserStore(store);
|
||||
const menuStore = useMenuStore(store);
|
||||
|
||||
|
|
@ -47,13 +48,13 @@ export function useAuthGuard(router: Router) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// 已登陆进行提示
|
||||
// 提示已登陆
|
||||
Notification.warning({
|
||||
title: '跳转提示',
|
||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||
});
|
||||
|
||||
// 不是从路由跳转的,跳转回首页
|
||||
// 直接访问跳转回首页(不是从路由跳转)
|
||||
if (!from.matched.length) {
|
||||
return '/';
|
||||
}
|
||||
|
|
@ -64,10 +65,13 @@ export function useAuthGuard(router: Router) {
|
|||
|
||||
// 未登录跳转到登陆页面
|
||||
if (!userStore.accessToken) {
|
||||
return { path: '/login', query: { redirect: to.path } };
|
||||
return {
|
||||
path: '/login',
|
||||
query: { redirect: to.path },
|
||||
};
|
||||
}
|
||||
|
||||
// 未获取菜单进行获取
|
||||
// 未获取权限进行获取
|
||||
if (!menuStore.menus.length) {
|
||||
// 菜单处理
|
||||
const authMenus = treeFilter(menus, item => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ declare module '@vue/runtime-core' {
|
|||
AAlert: typeof import('@arco-design/web-vue')['Alert']
|
||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||
ABadge: typeof import('@arco-design/web-vue')['Badge']
|
||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
|
|
|
|||
Loading…
Reference in New Issue