feat: 优化表格逻辑

master
luoer 2023-12-15 17:30:18 +08:00
parent aac4047c9a
commit 6a000652b1
54 changed files with 1078 additions and 876 deletions

View File

@ -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",

View File

@ -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

View File

@ -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?.());

View File

@ -11,6 +11,7 @@ export function addAuthInterceptor(axios: AxiosInstance) {
if (userStore.accessToken) {
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
}
// throw Error('dd');
return config;
});
}

View File

@ -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)) {

View File

@ -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);
}
);

View File

@ -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>

View File

@ -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;

View File

@ -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}
>
{{

View File

@ -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>;

View File

@ -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>;

View File

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

View File

@ -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>;

View File

@ -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,
};
}

View File

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

View File

@ -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,

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -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';

View File

@ -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>
</>
);
},

View File

View File

@ -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 });
}
};

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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>
</>
);

View File

@ -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);
}
}

View File

@ -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);
};
}
},

View File

@ -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,
};
}

View File

@ -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')
}
},
};
}

View File

@ -1,6 +1,10 @@
import { Block } from "../../core";
import { Font } from "../font";
export interface OutputText {
id: string;
}
export interface TextPrams {
/**
*

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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;
}
}
/**

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>
) : (

View File

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

View File

@ -1,4 +1,6 @@
<template><div></div></template>
<template>
<div></div>
</template>
<route lang="json">
{

View File

@ -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>

View File

@ -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>

View File

@ -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',

View File

@ -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 => {

View File

@ -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']