mirror of
https://github.com/jekip/naive-ui-admin.git
synced 2026-02-08 07:22:27 +08:00
Compare commits
308 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a469f1aca | ||
|
|
3bd0d28e05 | ||
|
|
447bdcc355 | ||
|
|
8bf849b803 | ||
|
|
02930e5208 | ||
|
|
ffca3ed61f | ||
|
|
0f92c953cb | ||
|
|
00247ee7b9 | ||
|
|
449761796c | ||
|
|
c96789f1ff | ||
|
|
301ca1a0df | ||
|
|
c5c28e958d | ||
|
|
2f97cbee06 | ||
|
|
764cb71f39 | ||
|
|
6d0aa46f20 | ||
|
|
f68ec16563 | ||
|
|
79c3cb5d4d | ||
|
|
7eb081ae87 | ||
|
|
cc2a911f2a | ||
|
|
b88c047643 | ||
|
|
01f9ba1046 | ||
|
|
f729a5b8ba | ||
|
|
7a62de39c2 | ||
|
|
1ae5372396 | ||
|
|
0bbc29023b | ||
|
|
84f55a8116 | ||
|
|
19df8d5f09 | ||
|
|
489f791caa | ||
|
|
2f72f34bbe | ||
|
|
3d4d554733 | ||
|
|
38fe255306 | ||
|
|
58e99c183b | ||
|
|
ee8aed2f62 | ||
|
|
260397f3ae | ||
|
|
fa983a9b64 | ||
|
|
4c49e28596 | ||
|
|
9a5fe5249c | ||
|
|
02235bf6fc | ||
|
|
0656035857 | ||
|
|
81e8222461 | ||
|
|
09411a927e | ||
|
|
347cd91735 | ||
|
|
9b2effbc55 | ||
|
|
d72f42dcd8 | ||
|
|
165c358ef5 | ||
|
|
fc181ce543 | ||
|
|
f04b9ba7f9 | ||
|
|
ee0e507e47 | ||
|
|
9d18715e90 | ||
|
|
74f0e4764e | ||
|
|
4cbdd9bc66 | ||
|
|
e03b1bdfca | ||
|
|
b6a350e1be | ||
|
|
503437a433 | ||
|
|
d4518849bf | ||
|
|
39178761b1 | ||
|
|
934de6b1e6 | ||
|
|
58d13a242d | ||
|
|
5d891c1f44 | ||
|
|
e1528823f7 | ||
|
|
1fa35db71b | ||
|
|
80729d925e | ||
|
|
e228184b72 | ||
|
|
2f541106f2 | ||
|
|
5f81f4cc77 | ||
|
|
b4f32f50c5 | ||
|
|
5d180b6fd5 | ||
|
|
3d6465346c | ||
|
|
dc98b96ce2 | ||
|
|
3f84af912f | ||
|
|
8dcd66b654 | ||
|
|
4bdc54a77f | ||
|
|
0ceb7437a4 | ||
|
|
b984828f33 | ||
|
|
1d10826760 | ||
|
|
74334de7e0 | ||
|
|
e153837497 | ||
|
|
d0ec44b48c | ||
|
|
e9efbb5e67 | ||
|
|
aa90e0a468 | ||
|
|
9049f743a8 | ||
|
|
1f858929d6 | ||
|
|
5a50bc0dc4 | ||
|
|
a35318f808 | ||
|
|
7a18f58f4a | ||
|
|
3fb8c85112 | ||
|
|
921dd91eda | ||
|
|
bcd2480f69 | ||
|
|
590d1615cb | ||
|
|
23dcfe000b | ||
|
|
47e47c8a59 | ||
|
|
719ad34a94 | ||
|
|
a0deba504a | ||
|
|
25d6134923 | ||
|
|
a646de72e2 | ||
|
|
e63963cf08 | ||
|
|
c43ec3ef1c | ||
|
|
4f0a7967f7 | ||
|
|
1c594b49c0 | ||
|
|
b68103455b | ||
|
|
724af94c08 | ||
|
|
af68d4bc67 | ||
|
|
2fb36695db | ||
|
|
32116be1f2 | ||
|
|
4a47fa2f7d | ||
|
|
89f68789ff | ||
|
|
4c0de9d5c2 | ||
|
|
1e72351dea | ||
|
|
da24659944 | ||
|
|
eb21a99df1 | ||
|
|
c598d3f7bc | ||
|
|
fa0163a385 | ||
|
|
57fef38bfe | ||
|
|
d62ca84328 | ||
|
|
c504e61d70 | ||
|
|
c0d3e11346 | ||
|
|
4c27623a0f | ||
|
|
0107c5f035 | ||
|
|
6134a4c125 | ||
|
|
00dd7409b1 | ||
|
|
a7ad4a3634 | ||
|
|
327f8de00b | ||
|
|
026e4c1acd | ||
|
|
9ba51e4a34 | ||
|
|
bc2d2b4e94 | ||
|
|
e2f90cbf88 | ||
|
|
2da9b8a465 | ||
|
|
f99ecea202 | ||
|
|
e3326a0f11 | ||
|
|
775f5b63af | ||
|
|
874fd3437d | ||
|
|
7644c59799 | ||
|
|
f8ed3d1607 | ||
|
|
0c8bb030de | ||
|
|
41a6f59617 | ||
|
|
1a7eaf7efb | ||
|
|
5714233d57 | ||
|
|
5e2452f2df | ||
|
|
e9d3e1affe | ||
|
|
f6d419b94f | ||
|
|
cc0d1824f2 | ||
|
|
299f8c89a5 | ||
|
|
9e40480b9b | ||
|
|
c20ee24cd8 | ||
|
|
eb27f19087 | ||
|
|
3cbb6061c5 | ||
|
|
e07e36ce5d | ||
|
|
0f8b0d3557 | ||
|
|
28d2696061 | ||
|
|
b2c22996ce | ||
|
|
f616852053 | ||
|
|
92316285bb | ||
|
|
1077a20cbd | ||
|
|
7b9b391adc | ||
|
|
88ada7ebee | ||
|
|
a7257568cf | ||
|
|
403a844668 | ||
|
|
197e63cb51 | ||
|
|
6c3273e214 | ||
|
|
71be58fc1f | ||
|
|
e3e14b8259 | ||
|
|
5f625aa305 | ||
|
|
8167d4165b | ||
|
|
873e0d3e43 | ||
|
|
f20f4209a3 | ||
|
|
363ed7ae9e | ||
|
|
a21cc8aec8 | ||
|
|
0f229aea13 | ||
|
|
89151afce3 | ||
|
|
ffac6c0d78 | ||
|
|
c257ca0948 | ||
|
|
9bcd6d9700 | ||
|
|
a66f56b9ce | ||
|
|
9e58578706 | ||
|
|
9e3ebd62b2 | ||
|
|
5b852506a6 | ||
|
|
f42857884f | ||
|
|
9ad5ba18a9 | ||
|
|
536e16f166 | ||
|
|
42f2256ea1 | ||
|
|
bf0d294322 | ||
|
|
51f5c64755 | ||
|
|
b49d9e8bd2 | ||
|
|
12e62d1179 | ||
|
|
6558e1597c | ||
|
|
7bf1e1265a | ||
|
|
1213de598e | ||
|
|
20c9dbbfe1 | ||
|
|
6dab2ab35b | ||
|
|
a424788c45 | ||
|
|
b31d5c2bd6 | ||
|
|
b979d9db32 | ||
|
|
a53c86e41b | ||
|
|
b16b5c8992 | ||
|
|
58dadbb95a | ||
|
|
4ebdbc7203 | ||
|
|
0729e56ed4 | ||
|
|
a50cbfa44d | ||
|
|
65d6d4d21e | ||
|
|
c5bb818f13 | ||
|
|
9e255da5d7 | ||
|
|
e2b5086be3 | ||
|
|
caaca83f78 | ||
|
|
91de971636 | ||
|
|
5fb005d5ae | ||
|
|
b42e0a2fef | ||
|
|
097dda5aa1 | ||
|
|
16714d4bdb | ||
|
|
a5438b4f50 | ||
|
|
4f5bbb0673 | ||
|
|
24cbde8b95 | ||
|
|
7222398cf0 | ||
|
|
261e27c139 | ||
|
|
8288f0a84b | ||
|
|
c0ff8985ea | ||
|
|
a8400ac475 | ||
|
|
d4b173e3c8 | ||
|
|
1ab8b5b221 | ||
|
|
fc5e138ed8 | ||
|
|
b9b2c6a07e | ||
|
|
3503569dfe | ||
|
|
125b7f44e3 | ||
|
|
3307b927a4 | ||
|
|
97de86eacb | ||
|
|
cb92f6e87b | ||
|
|
4f81743f90 | ||
|
|
7837c87392 | ||
|
|
17a5d08d94 | ||
|
|
19e5e5fc4a | ||
|
|
822e69deec | ||
|
|
3bf1e941d4 | ||
|
|
ef4912636e | ||
|
|
7929a74d20 | ||
|
|
45862d4f9d | ||
|
|
8e6471060c | ||
|
|
30c0cd5c95 | ||
|
|
259e73c056 | ||
|
|
8eaa889399 | ||
|
|
d1635add5f | ||
|
|
531a31ee5d | ||
|
|
3b64fc1563 | ||
|
|
229fc72cda | ||
|
|
900954d757 | ||
|
|
fca4dddaf6 | ||
|
|
4add3c3eaa | ||
|
|
21e80f8041 | ||
|
|
a63c34dbb5 | ||
|
|
e02e96d4e0 | ||
|
|
70c2b75f17 | ||
|
|
b186e045bc | ||
|
|
742749a838 | ||
|
|
82dc7d2589 | ||
|
|
f11af4fc76 | ||
|
|
85f1dbd5e9 | ||
|
|
ae001fc7bd | ||
|
|
be770016bf | ||
|
|
1bf722bed0 | ||
|
|
8c7dd14004 | ||
|
|
e6505c88b7 | ||
|
|
a0490e3b97 | ||
|
|
905984367c | ||
|
|
d3f7fa0f9e | ||
|
|
9c512002d2 | ||
|
|
737f967aab | ||
|
|
1cdb02c9d7 | ||
|
|
bc8dd21405 | ||
|
|
2dba60405e | ||
|
|
eba3047be2 | ||
|
|
0979b5af5d | ||
|
|
ade138997d | ||
|
|
d388ae5656 | ||
|
|
55ee389184 | ||
|
|
8f05b20ffa | ||
|
|
d973b2a543 | ||
|
|
1d5113a663 | ||
|
|
f331d9c4c7 | ||
|
|
c647e19d06 | ||
|
|
5c5c52d9fa | ||
|
|
3e0b8efe7e | ||
|
|
450234e7ea | ||
|
|
5116c387d5 | ||
|
|
8a5f237630 | ||
|
|
1e3ccaa6dc | ||
|
|
98e1bf0227 | ||
|
|
6a290b314a | ||
|
|
58f0997fb6 | ||
|
|
e602fc50c0 | ||
|
|
81a3e6d970 | ||
|
|
0c709871f3 | ||
|
|
9d9cac8064 | ||
|
|
b8f8334539 | ||
|
|
361a2a14c7 | ||
|
|
dd4e6c1670 | ||
|
|
85d39add87 | ||
|
|
20da92aeab | ||
|
|
57245d21ee | ||
|
|
f97a94e74c | ||
|
|
fd6fd723d7 | ||
|
|
da5231b384 | ||
|
|
97ae37efd0 | ||
|
|
b19430170f | ||
|
|
b642d28815 | ||
|
|
619669ec9e | ||
|
|
044976b790 | ||
|
|
b43ab1ceb4 | ||
|
|
f773a3ed06 | ||
|
|
7f81152793 | ||
|
|
f6be8f521e |
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=LF
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
[*.{yml,yaml,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
3
.env
3
.env
@@ -6,6 +6,3 @@ VITE_GLOB_APP_TITLE = AdminPro
|
|||||||
|
|
||||||
# spa shortname
|
# spa shortname
|
||||||
VITE_GLOB_APP_SHORT_NAME = AdminPro
|
VITE_GLOB_APP_SHORT_NAME = AdminPro
|
||||||
|
|
||||||
# 生产环境 开启mock
|
|
||||||
VITE_GLOB_PROD_MOCK = true
|
|
||||||
|
|||||||
@@ -4,23 +4,27 @@ VITE_PORT = 8001
|
|||||||
# 网站根目录
|
# 网站根目录
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# 是否开启mock
|
# 是否开启 mock
|
||||||
VITE_USE_MOCK = true
|
VITE_USE_MOCK = true
|
||||||
|
|
||||||
# 网站前缀
|
# 是否开启控制台打印 mock 请求信息
|
||||||
VITE_BASE_URL = /
|
VITE_LOGGER_MOCK = true
|
||||||
|
|
||||||
# 是否删除console
|
# 是否删除console
|
||||||
VITE_DROP_CONSOLE = true
|
VITE_DROP_CONSOLE = true
|
||||||
|
|
||||||
|
# 跨域代理,可以配置多个,请注意不要换行
|
||||||
|
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
|
||||||
|
#VITE_PROXY=[["/api","https://naive-ui-admin"]]
|
||||||
|
|
||||||
# API 接口地址
|
# API 接口地址
|
||||||
VITE_APP_API_URL = /
|
VITE_GLOB_API_URL =
|
||||||
|
|
||||||
# 图片上传地址
|
|
||||||
VITE_GLOB_UPLOAD_URL= /
|
|
||||||
|
|
||||||
# 图片前缀地址
|
|
||||||
VITE_GLOB_IMG_URL= /
|
|
||||||
|
|
||||||
# 接口前缀
|
# 接口前缀
|
||||||
VITE_GLOB_API_URL_PREFIX = /api
|
VITE_GLOB_API_URL_PREFIX = /api
|
||||||
|
|
||||||
|
# 文件上传地址
|
||||||
|
VITE_GLOB_UPLOAD_URL=
|
||||||
|
|
||||||
|
# 文件前缀地址
|
||||||
|
VITE_GLOB_FILE_URL=
|
||||||
|
|||||||
@@ -4,20 +4,25 @@ VITE_USE_MOCK = true
|
|||||||
# 网站根目录
|
# 网站根目录
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# 网站前缀
|
|
||||||
VITE_BASE_URL = /
|
|
||||||
|
|
||||||
# 是否删除console
|
# 是否删除console
|
||||||
VITE_DROP_CONSOLE = true
|
VITE_DROP_CONSOLE = true
|
||||||
|
|
||||||
# API
|
# API
|
||||||
VITE_APP_API_URL = /
|
VITE_GLOB_API_URL =
|
||||||
|
|
||||||
# 图片上传地址
|
|
||||||
VITE_GLOB_UPLOAD_URL= /
|
|
||||||
|
|
||||||
# 图片前缀地址
|
|
||||||
VITE_GLOB_IMG_URL= /
|
|
||||||
|
|
||||||
# 接口前缀
|
# 接口前缀
|
||||||
VITE_GLOB_API_URL_PREFIX = /api
|
VITE_GLOB_API_URL_PREFIX = /api
|
||||||
|
|
||||||
|
# 图片上传地址
|
||||||
|
VITE_GLOB_UPLOAD_URL=
|
||||||
|
|
||||||
|
# 图片前缀地址
|
||||||
|
VITE_GLOB_IMG_URL=
|
||||||
|
|
||||||
|
# 是否启用gzip压缩或brotli压缩
|
||||||
|
# 可选: gzip | brotli | none
|
||||||
|
# 如果你需要多种形式,你可以用','来分隔
|
||||||
|
VITE_BUILD_COMPRESS = 'none'
|
||||||
|
|
||||||
|
# 使用压缩时是否删除原始文件,默认为false
|
||||||
|
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
*.sh
|
*.sh
|
||||||
node_modules
|
node_modules
|
||||||
*.md
|
*.md
|
||||||
@@ -13,3 +12,5 @@ dist
|
|||||||
.local
|
.local
|
||||||
/bin
|
/bin
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
components.d.ts
|
||||||
|
components.d.ts
|
||||||
|
|||||||
68
.eslintrc.js
68
.eslintrc.js
@@ -1,9 +1,11 @@
|
|||||||
module.exports = {
|
// @ts-check
|
||||||
|
const { defineConfig } = require('eslint-define-config');
|
||||||
|
module.exports = defineConfig({
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
es6: true
|
es6: true,
|
||||||
},
|
},
|
||||||
parser: 'vue-eslint-parser',
|
parser: 'vue-eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@@ -12,21 +14,40 @@ module.exports = {
|
|||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
jsxPragma: 'React',
|
jsxPragma: 'React',
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true
|
jsx: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'prettier',
|
'prettier',
|
||||||
'plugin:prettier/recommended'
|
'plugin:prettier/recommended',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'vue/no-unused-components': 'off',
|
'vue/script-setup-uses-vars': 'error',
|
||||||
'vue/no-unused-vars': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/no-v-for-template-key-on-child': 'off',
|
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
'vue/custom-event-name-casing': 'off',
|
'vue/custom-event-name-casing': 'off',
|
||||||
// 'vue/attributes-order': 'off',
|
'no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/ban-types': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '.*', args: 'none' }],
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
// we are only using this rule to check for unused arguments since TS
|
||||||
|
// catches unused variables but not args.
|
||||||
|
{ varsIgnorePattern: '.*', args: 'none' },
|
||||||
|
],
|
||||||
|
'space-before-function-paren': 'off',
|
||||||
|
|
||||||
|
'vue/attributes-order': 'off',
|
||||||
'vue/one-component-per-file': 'off',
|
'vue/one-component-per-file': 'off',
|
||||||
'vue/html-closing-bracket-newline': 'off',
|
'vue/html-closing-bracket-newline': 'off',
|
||||||
'vue/max-attributes-per-line': 'off',
|
'vue/max-attributes-per-line': 'off',
|
||||||
@@ -34,34 +55,17 @@ module.exports = {
|
|||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'vue/attribute-hyphenation': 'off',
|
'vue/attribute-hyphenation': 'off',
|
||||||
'vue/require-default-prop': 'off',
|
'vue/require-default-prop': 'off',
|
||||||
|
|
||||||
'space-before-function-paren': 'off',
|
|
||||||
|
|
||||||
'@typescript-eslint/camelcase': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
|
||||||
'no-use-before-define': 'off',
|
|
||||||
'@typescript-eslint/no-use-before-define': 'off',
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
|
||||||
'@typescript-eslint/ban-types': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
|
||||||
'vue/html-self-closing': [
|
'vue/html-self-closing': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
html: {
|
html: {
|
||||||
void: 'always',
|
void: 'always',
|
||||||
normal: 'never',
|
normal: 'never',
|
||||||
component: 'always'
|
component: 'always',
|
||||||
},
|
},
|
||||||
svg: 'always',
|
svg: 'always',
|
||||||
math: 'always'
|
math: 'always',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,8 +18,11 @@ pnpm-debug.log*
|
|||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
!.vscode/extensions.json
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/components.d.ts
|
||||||
|
/components.d.ts
|
||||||
|
|||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"tu6ge.naive-ui-intelligence",
|
||||||
|
]
|
||||||
|
}
|
||||||
215
CHANGELOG.md
215
CHANGELOG.md
@@ -1,4 +1,209 @@
|
|||||||
# 1.4 (2021-07-21)
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 2.1.0
|
||||||
|
|
||||||
|
- 优化 `登录页面` 排版
|
||||||
|
- 新增 `构建分包策略`
|
||||||
|
- 新增 `useLocalSetting` hook
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
- 新增 `alova` 请求库
|
||||||
|
- 新增 `@faker-js/faker` 可配合 `mock` 数据模拟
|
||||||
|
- 新增 `VITE_USE_MOCK` 环境变量-开启 `mock`
|
||||||
|
- 新增 `demo` 实例,新增/编辑角色
|
||||||
|
- 移除 `axios` 请求封装,使用 `alova` 代替
|
||||||
|
- 移除 `vite-plugin-mock` 使用 `@alova/mock` 代替
|
||||||
|
- 移除 `VITE_GLOB_PROD_MOCK` 环境变量
|
||||||
|
- 变更 `VITE_GLOB_IMG_URL` 环境变量变更成 `VITE_GLOB_FILE_URL`
|
||||||
|
- 优化 `BasicTable` 组件相关样式
|
||||||
|
- 优化 `TS` 类型定义
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.9.2
|
||||||
|
|
||||||
|
- 升级 `vite` 到 `5.x` 版本
|
||||||
|
- 优化 `BasicTable` 组件,编辑样式
|
||||||
|
- 新增 `BasicTable` 组件,支持 `striped` 入参
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.9.1
|
||||||
|
|
||||||
|
- 优化 `typeSctipt` 类型定义
|
||||||
|
- 优化 `setup` 语法
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.9.0
|
||||||
|
|
||||||
|
- 新增 `BasicForm` 组件,支持 `setLoading`, `setSchema` 方法
|
||||||
|
- 新增 `countField` 总数字段名配置
|
||||||
|
- 优化 `yarn` 切换至 `pnpm`
|
||||||
|
- 优化 `BasicForm` 组件,验证返回值
|
||||||
|
- 优化 `BasicTable` 组件
|
||||||
|
- 修复 `TableAction组件,左右间隔不生效` 关闭[253](https://github.com/jekip/naive-ui-admin/issues/253)
|
||||||
|
- 修复 `BasicTable组件没有数据会一直请求接口` 关闭[#251](https://github.com/jekip/naive-ui-admin/issues/251)
|
||||||
|
- 修复 `useModal+useForm组件的bug` 关闭[#250](https://github.com/jekip/naive-ui-admin/issues/250)
|
||||||
|
- 修复 `手机端侧边导航风格不一致bug` 关闭[#247](https://github.com/jekip/naive-ui-admin/issues/247
|
||||||
|
- 移除 `yarn.lock` 文件
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.8.2
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `directive` 示例
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复 `样式异常` 图片
|
||||||
|
|
||||||
|
## 1.8.1
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `clean:cache` 删除缓存命令
|
||||||
|
- 新增 `clean:lib` 删除 `node_modules` 命令
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复 `开发环境` 运行控制台错误提示
|
||||||
|
|
||||||
|
## 1.8.0 (2022-04-01)
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `多页签` 支持配置 `affix` 固定属性
|
||||||
|
- 新增 `usePage` Hooks
|
||||||
|
- 表格列支持 `draggable` 配置拖拽 合并 [#114](https://github.com/jekip/naive-ui-admin/pull/114)
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复 `多页签` 关闭全部缺陷
|
||||||
|
- 修复 `多页签` 跳转缺陷(记得清空多页签缓存)
|
||||||
|
## 1.7.0 (2022-02-14)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 移除 `登录页面` 滑动验证组件
|
||||||
|
- 修复 `BasicUpload` 组件,回显问题
|
||||||
|
- 修复 `ts类型` 配置缺陷
|
||||||
|
- 修复 `登录页面` message 交互缺陷
|
||||||
|
- 修复 `表格编辑` 时间格式化异常 [#92](https://github.com/jekip/naive-ui-admin/issues/92)
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.6.1 (2022-01-06)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复 `项目配置` 打开空白
|
||||||
|
- 修复 `多标签` 背景和字体色变量丢失
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.6.0 (2021-12-24)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复 `低版本浏览器` 报 globalThis 未定义
|
||||||
|
- 修复 `Axios` api地址拼接异常
|
||||||
|
- 修复 `createStorage存在prefixKey` 会出bug
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 破坏 `Axios` 取消默认导出 `http` 可支持多个请求导出
|
||||||
|
- 搜索 `import http from '@/utils/http/axios'` 替换为 `import { http } from '@/utils/http/axios`
|
||||||
|
- 新增 `Axios` 多项配置 `urlPrefix`,`joinTime`,`ignoreCancelToken`,`withToken`,`uploadFile方法`
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
## 1.5.5 (2021-08-14)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复路由只存在一个子路由,图标不显示问题
|
||||||
|
- UI样式美化
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 支持 Vue 3.2.x
|
||||||
|
- 代码全部按 `script setup` 语法重写(完成80%)
|
||||||
|
- 新增 `回到顶部` 功能
|
||||||
|
- 新增 `拖拽` 示例页面
|
||||||
|
- 新增 `富文本` 组件
|
||||||
|
- 新增 `路由切换动画` 可在项目设置切换
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 1.5.4 (2021-08-10)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- `暗色模式下多页签背景问题 ` 合并 [#23](https://github.com/jekip/naive-ui-admin/pull/23) 感谢 [@Dishone](https://github.com/Dishone)
|
||||||
|
- `表格设置列,重复添加action列样式错乱问题` 合并 [#24](https://github.com/jekip/naive-ui-admin/pull/24) 感谢 [@CasbaL](https://github.com/CasbaL)
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `路由支持(内联外部地址)`配置
|
||||||
|
- 新增 `顶部菜单` logo展示
|
||||||
|
-(破坏性更新)
|
||||||
|
- 优化 `动态路由配置` 取消`constantRouterComponents.ts`,中组件映射配置,更名为 `router-icons.ts`
|
||||||
|
- 优化 `admin_info接口结构`,roles 更名为:permissions,roles.roleName,更名为:label
|
||||||
|
- 优化 多级路由,当没有配置`redirect`时,默认为第一个子路由,配置则优先按配置
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
# 1.5.3 (2021-08-09)
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复顶部菜单,选中联动
|
||||||
|
- 修复混合菜单模式,切换其他模式菜单未重置
|
||||||
|
- 实例基础列表,和表格组件实例,开启横向滚动特性
|
||||||
|
- `naiveui` 升级成最新版
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- table组件,默认开启 `ellipsis` 特性
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 1.5.2 (2021-08-06)
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复已知bug
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `混合菜单模式`
|
||||||
|
- 新增 `根路由`
|
||||||
|
- 新增 `关于` 根路由示例页面
|
||||||
|
- 文档同步更新,组件和示例
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 1.5.1 (2021-08-05)
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复windows系统获取项目换行符问题
|
||||||
|
- 修复表格分页计算问题 [@Chika99](https://github.com/Chika99)
|
||||||
|
- 修复锁屏样式自适应问题 [@Chika99](https://github.com/Chika99)
|
||||||
|
- 依赖 dayjs 移除,用date-fns,和UI框架底层保持一致
|
||||||
|
- 修复已知bug
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- 新增 `baseForm` 组件,和`基础`,`useForm`使用方式
|
||||||
|
- 新增 `baseModal`,组件,和 `useForm`使用方式
|
||||||
|
- 新增`子菜单` new Tag标签
|
||||||
|
- 菜单支持 `根路由`配置
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 1.5.0 (2021-07-30)
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- 修复表格列配置,拖拽时最后的操作列重复增加
|
||||||
|
- 多标签页交互优化
|
||||||
|
|
||||||
|
- ### ✨ Features
|
||||||
|
- `项目文档`已上线
|
||||||
|
- `Application`组件加载机制优化,解决路由守卫,Axios中可使用,Dialog,Message 等之类组件
|
||||||
|
- `BasicTable` 组件新增`高度自适应`,`单元格编辑`,`整行编辑` 特性
|
||||||
|
- `nprogress` 移除,用 `Loading Bar`代替
|
||||||
|
- 打包支持`gzip`,`brotli` 压缩
|
||||||
|
- 新增代理`VITE_PROXY`配置
|
||||||
|
- 路由菜单,支持多级菜单
|
||||||
|
- 依赖升级
|
||||||
|
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
|
||||||
|
|
||||||
|
|
||||||
|
# 1.4.0 (2021-07-21)
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
- vite降至2.3.6
|
- vite降至2.3.6
|
||||||
- 多标签页交互优化
|
- 多标签页交互优化
|
||||||
@@ -10,7 +215,7 @@
|
|||||||
- 持续更新更多实用组件及示例,感谢Star
|
- 持续更新更多实用组件及示例,感谢Star
|
||||||
|
|
||||||
|
|
||||||
# 1.3 (2021-07-19)
|
# 1.3.0 (2021-07-19)
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
- 修复多标签页左右切换按钮自适应展示
|
- 修复多标签页左右切换按钮自适应展示
|
||||||
- 修复登录页面出现多标签页
|
- 修复登录页面出现多标签页
|
||||||
@@ -23,7 +228,7 @@
|
|||||||
- 持续更新更多实用组件及示例,感谢Star
|
- 持续更新更多实用组件及示例,感谢Star
|
||||||
|
|
||||||
|
|
||||||
# 1.2 (2021-07-16)
|
# 1.2.0 (2021-07-16)
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
- 修复面包屑显示登录页面
|
- 修复面包屑显示登录页面
|
||||||
- 菜单支持只展开当前父级菜单
|
- 菜单支持只展开当前父级菜单
|
||||||
@@ -37,7 +242,7 @@
|
|||||||
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
||||||
|
|
||||||
|
|
||||||
# 1.1 (2021-07-15)
|
# 1.1.0 (2021-07-15)
|
||||||
- ### ✨ Features
|
- ### ✨ Features
|
||||||
- 新增 `基础表单` 示例页面
|
- 新增 `基础表单` 示例页面
|
||||||
- 新增 `分步表单` 示例页面
|
- 新增 `分步表单` 示例页面
|
||||||
@@ -45,7 +250,7 @@
|
|||||||
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
||||||
|
|
||||||
|
|
||||||
# 1.0 (2021-07-12)
|
# 1.0.0 (2021-07-12)
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
- 修复页面切换面包屑未及时更新
|
- 修复页面切换面包屑未及时更新
|
||||||
|
|
||||||
|
|||||||
123
README.md
123
README.md
@@ -1,46 +1,63 @@
|
|||||||
## 简介
|
## 🚀 简介
|
||||||
|
|
||||||
Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
|
`Naive Ui Admin` 是一款 完全免费 且可商用的中后台解决方案,基于 🌟 `Vue3.x` 🌟、🚀 `Vite` 🚀、✨ [Naive UI](https://www.naiveui.com/) ✨ 和 🎉 `TypeScript` 🎉。
|
||||||
|
它融合了最新的前端技术栈,提炼了典型的业务模型和页面,包括二次封装组件、动态菜单、权限校验等功能,助力快速搭建企业级中后台项目
|
||||||
|
|
||||||
## 特性
|
## 🌈 特性
|
||||||
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发
|
📦 二次封装的实用高扩展性组件
|
||||||
- **TypeScript**: 应用程序级 JavaScript 的语言
|
🎨 响应式、多主题、多配置,快速集成,开箱即用
|
||||||
- **主题**:可配置的主题
|
🚀 强大的鉴权系统,支持 三种鉴权模式,满足多样业务需求
|
||||||
- **Mock 数据** 内置 Mock 数据方案
|
🌐 持续更新的实用性页面模板和交互设计,简化页面构建
|
||||||
- **权限** 内置完善的动态路由权限生成方案
|
|
||||||
- **组件** 二次封装了多个常用的组件
|
|
||||||
|
|
||||||
### 页面功能
|
|
||||||
#### 系统看板
|
|
||||||
- [x] 主控台
|
|
||||||
- [ ] 监控页
|
|
||||||
- [x] 工作台
|
|
||||||
- [x] 表单页面
|
|
||||||
- [x] 列表页面
|
|
||||||
- [x] 异常页面
|
|
||||||
- [x] 结果页面
|
|
||||||
- [x] 设置页面
|
|
||||||
- [x] 系统设置
|
|
||||||
- [x] 菜单权限
|
|
||||||
- [x] 角色权限
|
|
||||||
|
|
||||||
### 页面组件
|
## 🎥 预览
|
||||||
#### ProTable
|
- [naive-ui-admin](https://gratis.naiveadmin.com)
|
||||||
- [x] 基础表格
|
|
||||||
- [x] 上传图片
|
|
||||||
- [x] 滑块验证码
|
|
||||||
- 持续开发中...
|
|
||||||
|
|
||||||
## 在线预览
|
账号:admin,密码:123456(随意)
|
||||||
- [naive-ui-admin](https://jekip.github.io)
|
|
||||||
|
|
||||||
账号:admin,密码:123456
|
|
||||||
|
|
||||||
## 文档
|
## 🚀 Naive Admin - 开箱即用的企业级前后端框架 `商业版本`
|
||||||
|
|
||||||
[文档地址](https://github.com/jekip/naive-ui-admin) - 待完善
|
> **✨ 多版本选择 · 四年持续迭代**
|
||||||
|
> 配套前后端支持 Java/Php 语言,支持单体和微服务多租户架构
|
||||||
|
> [详情→官网](https://www.naiveadmin.com) | [更新日志](https://www.yuque.com/u5825/zaqu0e)
|
||||||
|
|
||||||
## 准备
|
---
|
||||||
|
|
||||||
|
## 🔥 为什么选择 NaiveAdmin 商业版?
|
||||||
|
- **省时间**:内置丰富扩展组件与业务模板,不写一行样板代码即可开始业务开发
|
||||||
|
- **经实战**:已落地电网、跨境 ERP、SaaS 等 30+ 场景
|
||||||
|
- **可扩展**:插件式菜单 / 按钮 / 数据权限,新增业务模块「0 侵入」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ 纯前端版本
|
||||||
|
|
||||||
|
| 版本 | 技术栈 | 配套后端 | 预览地址 |
|
||||||
|
|-----|-------|---------|-------------|
|
||||||
|
| **🆕 Naive UI Max** | Vu3 + Ts + NaiveUI | 否 | [https://max.naiveadmin.com](https://max.naiveadmin.com) |
|
||||||
|
| **Naive UI Plus** | Vu3 + Ts + NaiveUI | 支持Java/PHP | [https://plus.naiveadmin.com](https://plus.naiveadmin.com) |
|
||||||
|
|
||||||
|
## 🔌 前后端版本
|
||||||
|
|
||||||
|
| 版本 | 技术栈 | 预览地址 |
|
||||||
|
|------|------------------|--------------------------------------------------------------|
|
||||||
|
| **🆕Naive UI Max** | Vu3 + Ts + NaiveUI | [https://max-full.naiveadmin.com](https://max-full.naiveadmin.com) |
|
||||||
|
| **Naive UI Plus** | Vu3 + Ts + NaiveUI | [https://plus-full.naiveadmin.com](https://plus-full.naiveadmin.com) |
|
||||||
|
|
||||||
|
## 🏢 多租户版本
|
||||||
|
|
||||||
|
| 版本 | 技术栈 | 适用场景 | 预览地址 |
|
||||||
|
|-----------------------------|-----------------------------|----------------|-------------------------------------------|
|
||||||
|
| **Vue3** | Vu3 + Ts + NaiveUI + Java | 构建企业级 Saas 化系统 | [https://tenant.naiveadmin.com](https://tenant.naiveadmin.com) |
|
||||||
|
| **React** | React + Ts + Ant + Java | 构建企业级 Saas 化系统 | [https://compose.warden.vip](https://compose.warden.vip) |
|
||||||
|
|
||||||
|
|
||||||
|
## 📚 文档
|
||||||
|
|
||||||
|
[开源版本文档](https://docs.naiveadmin.com)
|
||||||
|
|
||||||
|
## 🛠 准备
|
||||||
|
|
||||||
- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境
|
- [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境
|
||||||
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性
|
- [Vite](https://vitejs.dev/) - 熟悉 vite 特性
|
||||||
@@ -48,10 +65,11 @@ Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3
|
|||||||
- [TypeScript](https://www.typescriptlang.org/) - 熟悉`TypeScript`基本语法
|
- [TypeScript](https://www.typescriptlang.org/) - 熟悉`TypeScript`基本语法
|
||||||
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法
|
- [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法
|
||||||
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用
|
- [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用
|
||||||
- [Naive-ui-admin](https://www.naiveui.com/) - ui 基本使用
|
- [NaiveUi](https://www.naiveui.com/) - ui 基本使用
|
||||||
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
|
- [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法
|
||||||
|
|
||||||
## 安装使用
|
|
||||||
|
## 🏗️ 使用
|
||||||
|
|
||||||
- 获取项目代码
|
- 获取项目代码
|
||||||
|
|
||||||
@@ -64,33 +82,30 @@ git clone https://github.com/jekip/naive-ui-admin.git
|
|||||||
```bash
|
```bash
|
||||||
cd naive-ui-admin
|
cd naive-ui-admin
|
||||||
|
|
||||||
yarn install
|
pnpm install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- 运行
|
- 运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn dev
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- 打包
|
- 打包
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 更新日志
|
## 📜 更新日志
|
||||||
|
|
||||||
[CHANGELOG](./CHANGELOG.md)
|
[CHANGELOG](./CHANGELOG.md)
|
||||||
|
|
||||||
## 感谢
|
|
||||||
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,非常感谢作者。
|
|
||||||
|
|
||||||
|
## 🤝 如何贡献
|
||||||
|
|
||||||
## 如何贡献
|
非常欢迎你的加入 或者提交一个 `Pull Request`
|
||||||
|
|
||||||
非常欢迎你的加入 或者提交一个 Pull Request。
|
|
||||||
|
|
||||||
**Pull Request:**
|
**Pull Request:**
|
||||||
|
|
||||||
@@ -100,7 +115,7 @@ yarn build
|
|||||||
4. 推送您的分支: `git push origin feat/xxxx`
|
4. 推送您的分支: `git push origin feat/xxxx`
|
||||||
5. 提交`pull request`
|
5. 提交`pull request`
|
||||||
|
|
||||||
## Git 贡献提交规范
|
## 📋 Git 贡献提交规范
|
||||||
|
|
||||||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||||
|
|
||||||
@@ -118,7 +133,7 @@ yarn build
|
|||||||
- `types` 类型定义文件更改
|
- `types` 类型定义文件更改
|
||||||
- `wip` 开发中
|
- `wip` 开发中
|
||||||
|
|
||||||
## 浏览器支持
|
## 🌐 浏览器支持
|
||||||
|
|
||||||
本地开发推荐使用`Chrome 80+` 浏览器
|
本地开发推荐使用`Chrome 80+` 浏览器
|
||||||
|
|
||||||
@@ -128,12 +143,18 @@ yarn build
|
|||||||
| :-: | :-: | :-: | :-: | :-: |
|
| :-: | :-: | :-: | :-: | :-: |
|
||||||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||||
|
|
||||||
## 维护者
|
## 👥 维护者
|
||||||
[@Ah jung](https://github.com/jekip)
|
[@Ah jung](https://github.com/jekip)
|
||||||
|
|
||||||
## 交流
|
## 💬 交流
|
||||||
|
|
||||||
`Naive Ui Admin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。
|
有关 `Naive Ui Admin` 的使用或其他问题,可以加入讨论群交流问题
|
||||||
|
|
||||||
- QQ 群 `328347666`
|
QQ1群:328347666 (已满)
|
||||||
|
QQ2群:741353560
|
||||||
|
|
||||||
|
## 💖 赞助
|
||||||
|
#### 如果项目有帮到你,不妨请作者喝一杯咖啡吧!
|
||||||
|
|
||||||
|

|
||||||
|
[Paypal Me](https://www.paypal.com/paypalme/majunping)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @param env
|
* @param env
|
||||||
*/
|
*/
|
||||||
export const getConfigFileName = (env: Record<string, any>) => {
|
export const getConfigFileName = (env: Record<string, any>) => {
|
||||||
return `__PRODUCTION__${ env.VITE_GLOB_APP_SHORT_NAME || '__APP' }__CONF__`
|
return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.replace(/\s/g, '');
|
.replace(/\s/g, '');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ function createConfig(
|
|||||||
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} }
|
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const windowConf = `window.${ configName }`;
|
const windowConf = `window.${configName}`;
|
||||||
// Ensure that the variable will not be modified
|
// Ensure that the variable will not be modified
|
||||||
const configStr = `${ windowConf }=${ JSON.stringify(config) };
|
const configStr = `${windowConf}=${JSON.stringify(config)};
|
||||||
Object.freeze(${ windowConf });
|
Object.freeze(${windowConf});
|
||||||
Object.defineProperty(window, "${ configName }", {
|
Object.defineProperty(window, "${configName}", {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
writable: false,
|
writable: false,
|
||||||
});
|
});
|
||||||
`.replace(/\s/g, '');
|
`.replace(/\s/g, '');
|
||||||
fs.mkdirp(getRootPath(OUTPUT_DIR));
|
fs.mkdirp(getRootPath(OUTPUT_DIR));
|
||||||
writeFileSync(getRootPath(`${ OUTPUT_DIR }/${ configFileName }`), configStr);
|
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
|
||||||
|
|
||||||
console.log(chalk.cyan(`✨ [${ pkg.name }]`) + ` - configuration file is build successfully:`);
|
console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
|
||||||
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
|
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
|
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const runBuild = async () => {
|
|||||||
await runBuildConfig();
|
await runBuildConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✨ ${ chalk.cyan(`[${ pkg.name }]`) }` + ' - build successfully!');
|
console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(chalk.red('vite build error:\n' + error));
|
console.log(chalk.red('vite build error:\n' + error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
|
|||||||
if (envName === 'VITE_PROXY') {
|
if (envName === 'VITE_PROXY') {
|
||||||
try {
|
try {
|
||||||
realName = JSON.parse(realName);
|
realName = JSON.parse(realName);
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ret[envName] = realName;
|
ret[envName] = realName;
|
||||||
process.env[envName] = realName;
|
process.env[envName] = realName;
|
||||||
@@ -51,12 +50,11 @@ export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.pr
|
|||||||
try {
|
try {
|
||||||
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
|
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
|
||||||
envConfig = { ...envConfig, ...env };
|
envConfig = { ...envConfig, ...env };
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(envConfig).forEach((key) => {
|
Object.keys(envConfig).forEach((key) => {
|
||||||
const reg = new RegExp(`^(${ match })`);
|
const reg = new RegExp(`^(${match})`);
|
||||||
if (!reg.test(key)) {
|
if (!reg.test(key)) {
|
||||||
Reflect.deleteProperty(envConfig, key);
|
Reflect.deleteProperty(envConfig, key);
|
||||||
}
|
}
|
||||||
|
|||||||
35
build/vite/plugin/compress.ts
Normal file
35
build/vite/plugin/compress.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
|
||||||
|
* https://github.com/anncwb/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
|
||||||
|
import compressPlugin from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export function configCompressPlugin(
|
||||||
|
compress: 'gzip' | 'brotli' | 'none',
|
||||||
|
deleteOriginFile = false
|
||||||
|
): Plugin | Plugin[] {
|
||||||
|
const compressList = compress.split(',');
|
||||||
|
|
||||||
|
const plugins: Plugin[] = [];
|
||||||
|
|
||||||
|
if (compressList.includes('gzip')) {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.gz',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (compressList.includes('brotli')) {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.br',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
* Plugin to minimize and use ejs template syntax in index.html.
|
* Plugin to minimize and use ejs template syntax in index.html.
|
||||||
* https://github.com/anncwb/vite-plugin-html
|
* https://github.com/anncwb/vite-plugin-html
|
||||||
*/
|
*/
|
||||||
import type { Plugin } from 'vite';
|
import type { PluginOption } from 'vite';
|
||||||
|
|
||||||
import html from 'vite-plugin-html';
|
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||||
|
|
||||||
import pkg from '../../../package.json';
|
import pkg from '../../../package.json';
|
||||||
import { GLOB_CONFIG_FILE_NAME } from '../../constant';
|
import { GLOB_CONFIG_FILE_NAME } from '../../constant';
|
||||||
@@ -12,29 +12,29 @@ import { GLOB_CONFIG_FILE_NAME } from '../../constant';
|
|||||||
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
|
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
|
||||||
const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
|
const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
|
||||||
|
|
||||||
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${ VITE_PUBLIC_PATH }/`;
|
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;
|
||||||
|
|
||||||
const getAppConfigSrc = () => {
|
const getAppConfigSrc = () => {
|
||||||
return `${ path || '/' }${ GLOB_CONFIG_FILE_NAME }?v=${ pkg.version }-${ new Date().getTime() }`;
|
return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const htmlPlugin: Plugin[] = html({
|
const htmlPlugin: PluginOption[] = createHtmlPlugin({
|
||||||
minify: isBuild,
|
minify: isBuild,
|
||||||
inject: {
|
inject: {
|
||||||
// Inject data into ejs template
|
// Inject data into ejs template
|
||||||
injectData: {
|
data: {
|
||||||
title: VITE_GLOB_APP_TITLE,
|
title: VITE_GLOB_APP_TITLE,
|
||||||
},
|
},
|
||||||
// Embed the generated app.config.js file
|
// Embed the generated app.config.js file
|
||||||
tags: isBuild
|
tags: isBuild
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
tag: 'script',
|
tag: 'script',
|
||||||
attrs: {
|
attrs: {
|
||||||
src: getAppConfigSrc(),
|
src: getAppConfigSrc(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
import type { Plugin } from 'vite';
|
import type { Plugin, PluginOption } from 'vite';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
|
||||||
import { configHtmlPlugin } from './html';
|
import { configHtmlPlugin } from './html';
|
||||||
import { configMockPlugin } from './mock';
|
import { configCompressPlugin } from './compress';
|
||||||
|
|
||||||
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) {
|
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
|
||||||
const { VITE_USE_MOCK } = viteEnv;
|
const { VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
|
||||||
|
|
||||||
const vitePlugins: (Plugin | Plugin[])[] = [
|
const vitePlugins: (Plugin | Plugin[] | PluginOption[])[] = [
|
||||||
// have to
|
// have to
|
||||||
vue(),
|
vue(),
|
||||||
// have to
|
// have to
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
|
|
||||||
|
// 按需引入NaiveUi且自动创建组件声明
|
||||||
|
Components({
|
||||||
|
dts: true,
|
||||||
|
resolvers: [NaiveUiResolver()],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// vite-plugin-html
|
// vite-plugin-html
|
||||||
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));
|
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));
|
||||||
|
|
||||||
// vite-plugin-mock
|
if (isBuild) {
|
||||||
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock));
|
// rollup-plugin-gzip
|
||||||
|
vitePlugins.push(
|
||||||
|
configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return vitePlugins;
|
return vitePlugins;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mock plugin for development and production.
|
|
||||||
* https://github.com/anncwb/vite-plugin-mock
|
|
||||||
*/
|
|
||||||
import { viteMockServe } from 'vite-plugin-mock';
|
|
||||||
|
|
||||||
export function configMockPlugin(isBuild: boolean, prodMock: boolean) {
|
|
||||||
return viteMockServe({
|
|
||||||
ignore: /^\_/,
|
|
||||||
mockPath: 'mock',
|
|
||||||
localEnabled: !isBuild,
|
|
||||||
prodEnabled: isBuild && prodMock,
|
|
||||||
injectCode: `
|
|
||||||
import { setupProdMockServer } from '../mock/_createProductionServer';
|
|
||||||
|
|
||||||
setupProdMockServer();
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Introduces component library styles on demand.
|
|
||||||
* https://github.com/anncwb/vite-plugin-style-import
|
|
||||||
*/
|
|
||||||
|
|
||||||
import styleImport from 'vite-plugin-style-import';
|
|
||||||
|
|
||||||
export function configStyleImportPlugin(isBuild: boolean) {
|
|
||||||
if (!isBuild) return [];
|
|
||||||
const styleImportPlugin = styleImport({
|
|
||||||
libs: [
|
|
||||||
{
|
|
||||||
libraryName: 'ant-design-vue',
|
|
||||||
esModule: true,
|
|
||||||
resolveStyle: (name) => {
|
|
||||||
return `ant-design-vue/es/${ name }/style/index`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return styleImportPlugin;
|
|
||||||
}
|
|
||||||
34
build/vite/proxy.ts
Normal file
34
build/vite/proxy.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Used to parse the .env.development proxy configuration
|
||||||
|
*/
|
||||||
|
import type { ProxyOptions } from 'vite';
|
||||||
|
|
||||||
|
type ProxyItem = [string, string];
|
||||||
|
|
||||||
|
type ProxyList = ProxyItem[];
|
||||||
|
|
||||||
|
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
|
||||||
|
|
||||||
|
const httpsRE = /^https:\/\//;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate proxy
|
||||||
|
* @param list
|
||||||
|
*/
|
||||||
|
export function createProxy(list: ProxyList = []) {
|
||||||
|
const ret: ProxyTargetList = {};
|
||||||
|
for (const [prefix, target] of list) {
|
||||||
|
const isHttps = httpsRE.test(target);
|
||||||
|
|
||||||
|
// https://github.com/http-party/node-http-proxy#options
|
||||||
|
ret[prefix] = {
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
|
||||||
|
// https is require secure=false
|
||||||
|
...(isHttps ? { secure: false } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
ignores: [(commit) => commit.includes('init')],
|
|
||||||
extends: ['@commitlint/config-conventional'],
|
|
||||||
parserPreset: {
|
|
||||||
parserOpts: {
|
|
||||||
headerPattern: /^(\w*|[\u4e00-\u9fa5]*)(?:[\(\(](.*)[\)\)])?[\:\:] (.*)/,
|
|
||||||
headerCorrespondence: ['type', 'scope', 'subject'],
|
|
||||||
referenceActions: [
|
|
||||||
'close',
|
|
||||||
'closes',
|
|
||||||
'closed',
|
|
||||||
'fix',
|
|
||||||
'fixes',
|
|
||||||
'fixed',
|
|
||||||
'resolve',
|
|
||||||
'resolves',
|
|
||||||
'resolved'
|
|
||||||
],
|
|
||||||
issuePrefixes: ['#'],
|
|
||||||
noteKeywords: ['BREAKING CHANGE'],
|
|
||||||
fieldPattern: /^-(.*?)-$/,
|
|
||||||
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
|
|
||||||
revertCorrespondence: ['header', 'hash'],
|
|
||||||
warn() {},
|
|
||||||
mergePattern: null,
|
|
||||||
mergeCorrespondence: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'body-leading-blank': [2, 'always'],
|
|
||||||
'footer-leading-blank': [1, 'always'],
|
|
||||||
'header-max-length': [2, 'always', 108],
|
|
||||||
'subject-empty': [2, 'never'],
|
|
||||||
'type-empty': [2, 'never'],
|
|
||||||
'type-enum': [
|
|
||||||
2,
|
|
||||||
'always',
|
|
||||||
[
|
|
||||||
'feat',
|
|
||||||
'fix',
|
|
||||||
'perf',
|
|
||||||
'style',
|
|
||||||
'docs',
|
|
||||||
'test',
|
|
||||||
'refactor',
|
|
||||||
'build',
|
|
||||||
'ci',
|
|
||||||
'chore',
|
|
||||||
'revert',
|
|
||||||
'wip',
|
|
||||||
'workflow',
|
|
||||||
'types',
|
|
||||||
'release'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
117
index.html
117
index.html
@@ -1,25 +1,122 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-cmn-Hans">
|
<html lang="zh-cmn-Hans" id="htmlRoot" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
|
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"/>
|
||||||
<meta name="renderer" content="webkit"/>
|
<meta content="webkit" name="renderer"/>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
|
||||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
|
name="viewport"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="/favicon.ico"/>
|
<link href="/favicon.ico" rel="icon"/>
|
||||||
<title><%= title %></title>
|
<title><%= title %></title>
|
||||||
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="appProvider" style="display: none"></div>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="first-loading-wrp">
|
<style>
|
||||||
<div class="loading-wrp">
|
.first-loading-wrap {
|
||||||
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-loading-wrap > h1 {
|
||||||
|
font-size: 128px
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-loading-wrap .loading-wrap {
|
||||||
|
padding: 98px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
animation: antRotate 1.2s infinite linear;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 100%;
|
||||||
|
transform: scale(.75);
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
opacity: .3;
|
||||||
|
animation: antSpinMove 1s infinite linear alternate
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(1) {
|
||||||
|
top: 0;
|
||||||
|
left: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(2) {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
-webkit-animation-delay: .4s;
|
||||||
|
animation-delay: .4s
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(3) {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
-webkit-animation-delay: .8s;
|
||||||
|
animation-delay: .8s
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(4) {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
-webkit-animation-delay: 1.2s;
|
||||||
|
animation-delay: 1.2s
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes antRotate {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(405deg);
|
||||||
|
transform: rotate(405deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes antRotate {
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(405deg);
|
||||||
|
transform: rotate(405deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes antSpinMove {
|
||||||
|
to {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes antSpinMove {
|
||||||
|
to {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}</style>
|
||||||
|
<div class="first-loading-wrap">
|
||||||
|
<div class="loading-wrap">
|
||||||
|
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script>var globalThis = window;</script>
|
||||||
|
<script src="/src/main.ts" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
|
|
||||||
|
|
||||||
const modules = import.meta.globEager('./**/*.ts');
|
|
||||||
|
|
||||||
const mockModules: any[] = [];
|
|
||||||
Object.keys(modules).forEach((key) => {
|
|
||||||
if (key.includes('/_')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mockModules.push(...modules[key].default);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in a production environment. Need to manually import all modules
|
|
||||||
*/
|
|
||||||
export function setupProdMockServer() {
|
|
||||||
createProdMockServer(mockModules);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Mock from 'mockjs'
|
import Mock from 'mockjs';
|
||||||
|
|
||||||
export function resultSuccess(result, { message = 'ok' } = {}) {
|
export function resultSuccess(result, { message = 'ok' } = {}) {
|
||||||
return Mock.mock({
|
return Mock.mock({
|
||||||
@@ -50,10 +50,10 @@ export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]
|
|||||||
* @param {Number} times 回调函数需要执行的次数
|
* @param {Number} times 回调函数需要执行的次数
|
||||||
* @param {Function} callback 回调函数
|
* @param {Function} callback 回调函数
|
||||||
*/
|
*/
|
||||||
export function doCustomTimes (times:number, callback:any) {
|
export function doCustomTimes(times: number, callback: any) {
|
||||||
let i = -1
|
let i = -1;
|
||||||
while (++i < times) {
|
while (++i < times) {
|
||||||
callback(i)
|
callback(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
import { Random } from 'mockjs'
|
import { defineMock } from '@alova/mock';
|
||||||
import { resultSuccess } from '../_util'
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { resultSuccess } from '../_util';
|
||||||
|
|
||||||
const consoleInfo = {
|
function getRandom(options) {
|
||||||
//访问量
|
return Number(faker.commerce.price(options));
|
||||||
visits:{
|
|
||||||
dayVisits:Random.float(10000,99999,2,2),
|
|
||||||
rise:Random.float(10,99),
|
|
||||||
decline:Random.float(10,99),
|
|
||||||
amount:Random.float(99999,999999,3,5),
|
|
||||||
},
|
|
||||||
//销售额
|
|
||||||
saleroom:{
|
|
||||||
weekSaleroom:Random.float(10000,99999,2,2),
|
|
||||||
amount:Random.float(99999,999999,2,2),
|
|
||||||
degree:Random.float(10,99)
|
|
||||||
},
|
|
||||||
//订单量
|
|
||||||
orderLarge:{
|
|
||||||
weekLarge:Random.float(10000,99999,2,2),
|
|
||||||
rise:Random.float(10,99),
|
|
||||||
decline:Random.float(10,99),
|
|
||||||
amount:Random.float(99999,999999,2,2),
|
|
||||||
},
|
|
||||||
//成交额度
|
|
||||||
volume:{
|
|
||||||
weekLarge:Random.float(10000,99999,2,2),
|
|
||||||
rise:Random.float(10,99),
|
|
||||||
decline:Random.float(10,99),
|
|
||||||
amount:Random.float(99999,999999,2,2)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
//访问量
|
||||||
|
visits: {
|
||||||
|
dayVisits: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
},
|
||||||
|
//销售额
|
||||||
|
saleroom: {
|
||||||
|
weekSaleroom: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
degree: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
},
|
||||||
|
//订单量
|
||||||
|
orderLarge: {
|
||||||
|
weekLarge: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
},
|
||||||
|
//成交额度
|
||||||
|
volume: {
|
||||||
|
weekLarge: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
rise: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
decline: getRandom({ min: 10000, max: 99999, dec: 0 }),
|
||||||
|
amount: getRandom({ min: 10000, max: 99999, dec: 2 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default [
|
export default defineMock({
|
||||||
//主控台 卡片数据
|
// 主控台数据
|
||||||
{
|
'/api/dashboard/console': () => {
|
||||||
url: '/api/dashboard/console',
|
return resultSuccess(result);
|
||||||
timeout: 1000,
|
},
|
||||||
method: 'get',
|
});
|
||||||
response: () => {
|
|
||||||
return resultSuccess(consoleInfo);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +1,86 @@
|
|||||||
import { resultSuccess } from '../_util'
|
import { defineMock } from '@alova/mock';
|
||||||
|
import { resultSuccess } from '../_util';
|
||||||
|
import type { ListDate } from '@/api/system/menu';
|
||||||
|
|
||||||
const menuList = (() => {
|
const menuList = () => {
|
||||||
const result: any[] = [
|
const result: ListDate[] = [
|
||||||
{
|
|
||||||
label: 'Dashboard',
|
|
||||||
key: 'dashboard',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'dashboard',
|
|
||||||
openType:1,
|
|
||||||
auth:'dashboard',
|
|
||||||
path:'/dashboard',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: '主控台',
|
|
||||||
key: 'console',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'console',
|
|
||||||
openType:1,
|
|
||||||
auth:'console',
|
|
||||||
path:'/dashboard/console',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '工作台',
|
|
||||||
key: 'workplace',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'workplace',
|
|
||||||
openType:1,
|
|
||||||
auth:'workplace',
|
|
||||||
path:'/dashboard/workplace',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '表单管理',
|
|
||||||
key: 'form',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'form',
|
|
||||||
openType:1,
|
|
||||||
auth:'form',
|
|
||||||
path:'/form',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: '基础表单',
|
|
||||||
key: 'basic-form',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'basic-form',
|
|
||||||
openType:1,
|
|
||||||
auth:'basic-form',
|
|
||||||
path:'/form/basic-form',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '分步表单',
|
|
||||||
key: 'step-form',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'step-form',
|
|
||||||
openType:1,
|
|
||||||
auth:'step-form',
|
|
||||||
path:'/form/step-form',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '表单详情',
|
|
||||||
key: 'detail',
|
|
||||||
type: 1,
|
|
||||||
subtitle:'detail',
|
|
||||||
openType:1,
|
|
||||||
auth:'detail',
|
|
||||||
path:'/form/detail',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return result
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
{
|
||||||
url: '/api/menu/list',
|
label: 'Dashboard',
|
||||||
timeout: 1000,
|
key: 'dashboard',
|
||||||
method: 'get',
|
type: 1,
|
||||||
response: () => {
|
subtitle: 'dashboard',
|
||||||
const list = menuList()
|
openType: 1,
|
||||||
return resultSuccess({
|
auth: 'dashboard',
|
||||||
list
|
path: '/dashboard',
|
||||||
}
|
children: [
|
||||||
);
|
{
|
||||||
|
label: '主控台',
|
||||||
|
key: 'console',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'console',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'console',
|
||||||
|
path: '/dashboard/console',
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
]
|
label: '工作台',
|
||||||
|
key: 'workplace',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'workplace',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'workplace',
|
||||||
|
path: '/dashboard/workplace',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '表单管理',
|
||||||
|
key: 'form',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'form',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'form',
|
||||||
|
path: '/form',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '基础表单',
|
||||||
|
key: 'basic-form',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'basic-form',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'basic-form',
|
||||||
|
path: '/form/basic-form',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '分步表单',
|
||||||
|
key: 'step-form',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'step-form',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'step-form',
|
||||||
|
path: '/form/step-form',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '表单详情',
|
||||||
|
key: 'detail',
|
||||||
|
type: 1,
|
||||||
|
subtitle: 'detail',
|
||||||
|
openType: 1,
|
||||||
|
auth: 'detail',
|
||||||
|
path: '/form/detail',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineMock({
|
||||||
|
'/api/menu/list': () => {
|
||||||
|
const list = menuList();
|
||||||
|
return resultSuccess({
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,50 +1,45 @@
|
|||||||
import { resultSuccess, doCustomTimes } from '../_util'
|
import { defineMock } from '@alova/mock';
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { resultSuccess, doCustomTimes } from '../_util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
function getMenuKeys() {
|
function getMenuKeys() {
|
||||||
let keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail']
|
const keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail'];
|
||||||
let newKeys = []
|
const newKeys = [];
|
||||||
doCustomTimes(parseInt(Math.random()*6), () => {
|
doCustomTimes(parseInt(Math.random() * 6), () => {
|
||||||
let key = keys[Math.floor(Math.random() * keys.length)];
|
const key = keys[Math.floor(Math.random() * keys.length)];
|
||||||
newKeys.push(key)
|
newKeys.push(key as never);
|
||||||
})
|
});
|
||||||
return Array.from(new Set(newKeys));
|
return Array.from(new Set(newKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleList = ((pageSize) => {
|
const roleList = (pageSize) => {
|
||||||
const result: any[] = []
|
const result: any[] = [];
|
||||||
doCustomTimes(pageSize, () => {
|
doCustomTimes(pageSize, () => {
|
||||||
result.push({
|
result.push({
|
||||||
id: '@integer(10,100)',
|
id: faker.string.numeric(4),
|
||||||
name: '@cname()',
|
name: faker.person.firstName(),
|
||||||
explain: '@cname()',
|
explain: faker.lorem.sentence({ min: 2, max: 4 }),
|
||||||
isDefault: '@boolean()',
|
isDefault: faker.helpers.arrayElement([true, false]),
|
||||||
menu_keys: getMenuKeys(),
|
menu_keys: getMenuKeys(),
|
||||||
create_date: `@date('yyyy-MM-dd hh:mm:ss')`,
|
create_date: dayjs(faker.date.anytime()).format('YYYY-MM-DD HH:mm'),
|
||||||
'status|1': ['normal', 'enable', 'disable'],
|
status: faker.helpers.arrayElement(['normal', 'enable', 'disable']),
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
return result
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineMock({
|
||||||
|
'/api/role/list': ({ query }) => {
|
||||||
|
const { page = 1, pageSize = 10, name } = query;
|
||||||
|
const list = roleList(Number(pageSize));
|
||||||
|
// 并非真实,只是为了模拟搜索结果
|
||||||
|
const count = name ? 30 : 60;
|
||||||
|
return resultSuccess({
|
||||||
|
page: Number(page),
|
||||||
|
pageSize: Number(pageSize),
|
||||||
|
pageCount: count,
|
||||||
|
itemCount: count * Number(pageSize),
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
url: '/api/role/list',
|
|
||||||
timeout: 1000,
|
|
||||||
method: 'get',
|
|
||||||
response: ({ query }) => {
|
|
||||||
const { page = 1, pageSize = 10 } = query;
|
|
||||||
const list = roleList(Number(pageSize))
|
|
||||||
return resultSuccess({
|
|
||||||
page: Number(page),
|
|
||||||
pageSize: Number(pageSize),
|
|
||||||
pageCount: 60,
|
|
||||||
list
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,39 @@
|
|||||||
import { Random } from 'mockjs'
|
import { defineMock } from '@alova/mock';
|
||||||
import { resultSuccess, doCustomTimes, resultPageSuccess } from '../_util'
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { doCustomTimes, resultSuccess } from '../_util';
|
||||||
const tableList = ((pageSize) => {
|
import dayjs from 'dayjs';
|
||||||
const result:any[] = []
|
function tableList(pageSize: number) {
|
||||||
doCustomTimes(pageSize,()=> {
|
const result: any[] = [];
|
||||||
|
doCustomTimes(pageSize, () => {
|
||||||
result.push({
|
result.push({
|
||||||
id: '@integer(10,100)',
|
id: faker.string.numeric(4),
|
||||||
beginTime: '@datetime',
|
name: faker.person.firstName(),
|
||||||
endTime: '@datetime',
|
sex: faker.person.sexType(),
|
||||||
address: '@city()',
|
avatar: `https://picsum.photos/200/200?v=${faker.string.numeric(4)}`,
|
||||||
name: '@cname()',
|
email: faker.internet.email({ firstName: 'admin' }),
|
||||||
avatar: Random.image('400x400', Random.color(), Random.color(), Random.first()),
|
city: faker.location.city(),
|
||||||
date: `@date('yyyy-MM-dd')`,
|
status: faker.helpers.arrayElement(['close', 'refuse', 'pass']),
|
||||||
time: `@time('HH:mm')`,
|
type: faker.helpers.arrayElement(['person', 'company']),
|
||||||
'no|100000-10000000': 100000,
|
// createDate: faker.helpers.arrayElement(dateStrs),
|
||||||
'status|1': ['normal', 'enable', 'disable'],
|
createDate: dayjs(faker.date.anytime()).format('YYYY-MM-DD HH:mm'),
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
return result
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineMock({
|
||||||
|
// 表格数据列表
|
||||||
|
'/api/table/list': ({ query }) => {
|
||||||
|
const { page = 1, pageSize = 10, name } = query;
|
||||||
|
const list = tableList(Number(pageSize));
|
||||||
|
// 并非真实,只是为了模拟搜索结果
|
||||||
|
const count = name ? 30 : 60;
|
||||||
|
return resultSuccess({
|
||||||
|
page: Number(page),
|
||||||
|
pageSize: Number(pageSize),
|
||||||
|
pageCount: count,
|
||||||
|
itemCount: count * Number(pageSize),
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default [
|
|
||||||
//表格数据列表
|
|
||||||
{
|
|
||||||
url: '/api/table/list',
|
|
||||||
timeout: 1000,
|
|
||||||
method: 'get',
|
|
||||||
response: ({ query }) => {
|
|
||||||
const { page = 1, pageSize = 10 } = query;
|
|
||||||
const list = tableList(Number(pageSize))
|
|
||||||
return resultSuccess({
|
|
||||||
page:Number(page),
|
|
||||||
pageSize:Number(pageSize),
|
|
||||||
pageCount: 60,
|
|
||||||
list
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
44
mock/user/index.ts
Normal file
44
mock/user/index.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Mock from 'mockjs';
|
||||||
|
import { resultSuccess } from '../_util';
|
||||||
|
import { defineMock } from '@alova/mock';
|
||||||
|
|
||||||
|
const Random = Mock.Random;
|
||||||
|
|
||||||
|
const token = Random.string('upper', 32, 32);
|
||||||
|
|
||||||
|
const adminInfo = {
|
||||||
|
userId: '1',
|
||||||
|
username: 'admin',
|
||||||
|
realName: 'Admin',
|
||||||
|
avatar: Random.image(),
|
||||||
|
desc: 'manager',
|
||||||
|
password: Random.string('upper', 4, 16),
|
||||||
|
token,
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
label: '主控台',
|
||||||
|
value: 'dashboard_console',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '监控页',
|
||||||
|
value: 'dashboard_monitor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '工作台',
|
||||||
|
value: 'dashboard_workplace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '基础列表',
|
||||||
|
value: 'basic_list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '基础列表删除',
|
||||||
|
value: 'basic_list_delete',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineMock({
|
||||||
|
'[POST]/api/login': () => resultSuccess({ token }),
|
||||||
|
'/api/admin_info': () => resultSuccess(adminInfo),
|
||||||
|
});
|
||||||
@@ -1,53 +1,46 @@
|
|||||||
import { MockMethod } from 'vite-plugin-mock'
|
import { defineMock } from '@alova/mock';
|
||||||
import { resultSuccess, getRequestToken } from '../_util'
|
import { resultSuccess } from '../_util';
|
||||||
|
|
||||||
const menusList = [
|
const menusList = [
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: 'Layout',
|
component: 'LAYOUT',
|
||||||
redirect: '/dashboard/console',
|
redirect: '/dashboard/console',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'DashboardOutlined',
|
icon: 'DashboardOutlined',
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'console',
|
path: 'console',
|
||||||
name: 'dashboard_console',
|
name: 'dashboard_console',
|
||||||
component: 'DashboardConsole',
|
component: '/dashboard/console/console',
|
||||||
meta: {
|
meta: {
|
||||||
title: '主控台',
|
title: '主控台',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'monitor',
|
path: 'monitor',
|
||||||
name: 'dashboard_monitor',
|
name: 'dashboard_monitor',
|
||||||
component: 'DashboardMonitor',
|
component: '/dashboard/monitor/monitor',
|
||||||
meta: {
|
meta: {
|
||||||
title: '监控页',
|
title: '监控页',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workplace',
|
path: 'workplace',
|
||||||
name: 'dashboard_workplace',
|
name: 'dashboard_workplace',
|
||||||
component: 'DashboardWorkplace',
|
component: '/dashboard/workplace/workplace',
|
||||||
meta: {
|
meta: {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
title: '工作台',
|
title: '工作台',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export default [
|
export default defineMock({
|
||||||
{
|
'/api/menus': () => resultSuccess(menusList),
|
||||||
url: '/api/menus',
|
});
|
||||||
timeout: 1000,
|
|
||||||
method: 'get',
|
|
||||||
response: () => {
|
|
||||||
return resultSuccess(menusList);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import Mock from 'mockjs'
|
|
||||||
import { resultSuccess, getRequestToken } from '../_util'
|
|
||||||
|
|
||||||
const Random = Mock.Random
|
|
||||||
|
|
||||||
const token = Random.string('upper', 32, 32)
|
|
||||||
|
|
||||||
const adminInfo = {
|
|
||||||
userId: '1',
|
|
||||||
username: 'admin',
|
|
||||||
realName: 'Admin',
|
|
||||||
avatar: Random.image(),
|
|
||||||
desc: 'manager',
|
|
||||||
password: Random.string('upper', 4, 16),
|
|
||||||
token,
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
roleName: '主控台',
|
|
||||||
value: 'dashboard_console',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleName: '监控页',
|
|
||||||
value: 'dashboard_monitor',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleName: '工作台',
|
|
||||||
value: 'dashboard_workplace',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleName: '基础列表',
|
|
||||||
value: 'basic_list',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleName: '基础列表删除',
|
|
||||||
value: 'basic_list_delete',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
url: '/api/login',
|
|
||||||
timeout: 1000,
|
|
||||||
method: 'post',
|
|
||||||
response: () => {
|
|
||||||
return resultSuccess({ token });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: '/api/admin_info',
|
|
||||||
timeout: 1000,
|
|
||||||
method: 'get',
|
|
||||||
response: () => {
|
|
||||||
// const token = getRequestToken(request);
|
|
||||||
// if (!token) return resultError('Invalid token');
|
|
||||||
return resultSuccess(adminInfo);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
144
package.json
144
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "naive-ui-admin",
|
"name": "naive-ui-admin",
|
||||||
"version": "1.4",
|
"version": "2.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Ahjung",
|
"name": "Ahjung",
|
||||||
"email": "735878602@qq.com",
|
"email": "735878602@qq.com",
|
||||||
@@ -8,83 +8,93 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"bootstrap": "pnpm install",
|
||||||
|
"serve": "pnpm run dev",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build && esno ./build/script/postBuild.ts",
|
"build": "vite build && esno ./build/script/postBuild.ts",
|
||||||
"preview": "vite preview",
|
"build:no-cache": "pnpm clean:cache && pnpm run build",
|
||||||
"build typecheck": "vuedx-typecheck . && vite build",
|
"report": "cross-env REPORT=true pnpm run build",
|
||||||
|
"preview": "pnpm run build && vite preview",
|
||||||
|
"preview:dist": "vite preview",
|
||||||
|
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
|
||||||
|
"clean:lib": "rimraf node_modules",
|
||||||
"deploy": "gh-pages -d dist",
|
"deploy": "gh-pages -d dist",
|
||||||
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
|
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
|
||||||
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
"lint:stylelint": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
"lint:stylelint": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
||||||
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
|
"lint:pretty": "pretty-quick --staged"
|
||||||
"lint:pretty": "pretty-quick --staged",
|
|
||||||
"test prod gzip": "http-server dist --cors --gzip -c-1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/antd": "^0.10.0",
|
"@alova/mock": "^2.0.17",
|
||||||
"@vicons/ionicons5": "^0.10.0",
|
"@vicons/antd": "^0.12.0",
|
||||||
"@vueuse/core": "^5.0.3",
|
"@vicons/ionicons5": "^0.12.0",
|
||||||
"axios": "^0.21.1",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"blueimp-md5": "^2.18.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"dayjs": "^1.10.5",
|
"alova": "^3.3.4",
|
||||||
"echarts": "^5.1.2",
|
"date-fns": "^2.30.0",
|
||||||
"element-resize-detector": "^1.2.3",
|
"dayjs": "^1.11.18",
|
||||||
"lodash": "^4.17.21",
|
"echarts": "^5.6.0",
|
||||||
|
"element-resize-detector": "^1.2.4",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"makeit-captcha": "^1.2.5",
|
|
||||||
"mitt": "^2.1.0",
|
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"naive-ui": "^2.15.5",
|
"naive-ui": "^2.43.1",
|
||||||
"nprogress": "^1.0.0-1",
|
"pinia": "^2.3.1",
|
||||||
"pinia": "^2.0.0-beta.3",
|
"qs": "^6.14.0",
|
||||||
"qs": "^6.10.1",
|
"vue": "^3.5.21",
|
||||||
"vfonts": "^0.1.0",
|
"vue-router": "^4.5.1",
|
||||||
"vue": "^3.1.2",
|
"vue-types": "^4.2.1"
|
||||||
"vue-router": "^4.0.10",
|
|
||||||
"vue-types": "^4.0.0",
|
|
||||||
"vuedraggable": "^4.0.3",
|
|
||||||
"vuex": "^4.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^12.1.4",
|
"@commitlint/cli": "^17.8.1",
|
||||||
"@commitlint/config-conventional": "^12.1.4",
|
"@commitlint/config-conventional": "^17.8.1",
|
||||||
"@types/lodash": "^4.14.170",
|
"@faker-js/faker": "^9.9.0",
|
||||||
"@types/node": "^15.12.2",
|
"@types/lodash": "^4.17.20",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@typescript-eslint/parser": "^4.26.1",
|
"@types/node": "^18.19.126",
|
||||||
"@vitejs/plugin-vue": "^1.2.3",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.1.5",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vue/compiler-sfc": "3.1.1",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vitejs/plugin-vue-jsx": "^2.1.1",
|
||||||
"autoprefixer": "^10.3.1",
|
"@vue/compiler-sfc": "^3.5.21",
|
||||||
"commitizen": "^4.2.4",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"core-js": "^3.14.0",
|
"autoprefixer": "^10.4.21",
|
||||||
"dotenv": "^10.0.0",
|
"commitizen": "^4.3.1",
|
||||||
"eslint": "^7.28.0",
|
"core-js": "^3.45.1",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"cross-env": "^7.0.3",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"dotenv": "^16.6.1",
|
||||||
"eslint-plugin-vue": "^7.11.1",
|
"eslint": "^8.57.1",
|
||||||
"esno": "^0.7.3",
|
"eslint-config-prettier": "^8.10.2",
|
||||||
"gh-pages": "^3.2.0",
|
"eslint-define-config": "1.12.0",
|
||||||
"husky": "^6.0.0",
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
"less": "^4.1.1",
|
"eslint-plugin-prettier": "^4.2.5",
|
||||||
"less-loader": "^9.0.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"lint-staged": "^11.0.0",
|
"esno": "^4.8.0",
|
||||||
"postcss": "^8.3.5",
|
"gh-pages": "^4.0.0",
|
||||||
"prettier": "^2.3.1",
|
"husky": "^8.0.3",
|
||||||
"pretty-quick": "^3.1.0",
|
"jest": "^29.7.0",
|
||||||
"stylelint": "^13.13.1",
|
"less": "^4.4.1",
|
||||||
"stylelint-config-prettier": "^8.0.2",
|
"less-loader": "^11.1.4",
|
||||||
"stylelint-config-standard": "^22.0.0",
|
"lint-staged": "^13.3.0",
|
||||||
"stylelint-order": "^4.1.0",
|
"postcss": "^8.5.6",
|
||||||
"stylelint-scss": "^3.19.0",
|
"prettier": "^2.8.8",
|
||||||
"tailwindcss": "^2.2.4",
|
"pretty-quick": "^3.3.1",
|
||||||
"typescript": "^4.3.2",
|
"rimraf": "^3.0.2",
|
||||||
"vite": "2.3.6",
|
"stylelint": "^14.16.1",
|
||||||
"vite-plugin-html": "^2.0.7",
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
"vite-plugin-mock": "^2.9.3",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"vite-plugin-style-import": "^1.0.1",
|
"stylelint-order": "^5.0.0",
|
||||||
"vue-eslint-parser": "^7.8.0"
|
"stylelint-scss": "^4.7.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"unplugin-vue-components": "^0.22.12",
|
||||||
|
"vite": "^5.4.20",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-html": "^3.2.2",
|
||||||
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
|
"vue-demi": "^0.13.11",
|
||||||
|
"vue-draggable-next": "^2.3.0",
|
||||||
|
"vue-eslint-parser": "^9.4.3",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{vue,js,ts,tsx}": "eslint --fix"
|
"*.{vue,js,ts,tsx}": "eslint --fix"
|
||||||
@@ -114,6 +124,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/jekip/naive-ui-admin",
|
"homepage": "https://github.com/jekip/naive-ui-admin",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12 || >=14"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9220
pnpm-lock.yaml
generated
Normal file
9220
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ module.exports = {
|
|||||||
requirePragma: false,
|
requirePragma: false,
|
||||||
proseWrap: 'never',
|
proseWrap: 'never',
|
||||||
htmlWhitespaceSensitivity: 'strict',
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
endOfLine: 'lf',
|
endOfLine: 'auto',
|
||||||
rangeStart: 0,
|
rangeStart: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="renderer" content="webkit"/>
|
|
||||||
<meta name="force-rendering" content="webkit"/>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
|
|
||||||
<meta name="referrer" content="no-referrer" />
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<script src="<%= BASE_URL %>iconfont.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
|
||||||
Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
<!-- IE 浏览器跳转提示升级页面 start -->
|
|
||||||
<script>
|
|
||||||
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode)) {
|
|
||||||
window.open("https://support.dmeng.net/upgrade-your-browser.html?referrer="+encodeURIComponent(window.location.href));
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<!-- IE 浏览器跳转提示升级页面 end -->
|
|
||||||
<script>
|
|
||||||
function isIE() {
|
|
||||||
if (!!window.ActiveXObject || "ActiveXObject" in window){
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
160
src/App.vue
160
src/App.vue
@@ -1,105 +1,83 @@
|
|||||||
<template>
|
<template>
|
||||||
<NConfigProvider
|
<NConfigProvider
|
||||||
v-if="!isLock"
|
v-if="!isLock"
|
||||||
:locale="zhCN"
|
:locale="zhCN"
|
||||||
:theme="getDarkTheme"
|
:theme="getDarkTheme"
|
||||||
:theme-overrides="getThemeOverrides"
|
:theme-overrides="getThemeOverrides"
|
||||||
:date-locale="dateZhCN"
|
:date-locale="dateZhCN"
|
||||||
>
|
>
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<RouterView/>
|
<RouterView />
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</NConfigProvider>
|
</NConfigProvider>
|
||||||
|
|
||||||
<transition v-if="isLock && $route.name != 'login'" name="slide-up">
|
<transition v-if="isLock && $route.name !== 'login'" name="slide-up">
|
||||||
<LockScreen/>
|
<LockScreen />
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, computed, onMounted, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui'
|
import { zhCN, dateZhCN, darkTheme } from 'naive-ui';
|
||||||
import { LockScreen } from '@/components/Lockscreen'
|
import { LockScreen } from '@/components/Lockscreen';
|
||||||
import { AppProvider } from '@/components/Application'
|
import { AppProvider } from '@/components/Application';
|
||||||
import { useLockscreenStore } from '@/store/modules/lockscreen'
|
import { useScreenLockStore } from '@/store/modules/screenLock.js';
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router';
|
||||||
import { useDesignSettingStore } from '@/store/modules/designSetting'
|
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||||
|
import { lighten } from '@/utils/index';
|
||||||
|
|
||||||
export default defineComponent({
|
const route = useRoute();
|
||||||
name: 'App',
|
const useScreenLock = useScreenLockStore();
|
||||||
components: { LockScreen, AppProvider },
|
const designStore = useDesignSettingStore();
|
||||||
setup() {
|
const isLock = computed(() => useScreenLock.isLocked);
|
||||||
const route = useRoute()
|
const lockTime = computed(() => useScreenLock.lockTime);
|
||||||
const useLockscreen = useLockscreenStore()
|
|
||||||
const designStore = useDesignSettingStore()
|
|
||||||
const isLock = computed(() => useLockscreen.isLock)
|
|
||||||
const lockTime = computed(() => useLockscreen.lockTime)
|
|
||||||
/**
|
|
||||||
* @type import('naive-ui').GlobalThemeOverrides
|
|
||||||
*/
|
|
||||||
const getThemeOverrides = computed(() => {
|
|
||||||
return {
|
|
||||||
common: {
|
|
||||||
primaryColor: designStore.appTheme,
|
|
||||||
primaryColorHover: '#57a3f3'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined))
|
|
||||||
|
|
||||||
let timer
|
|
||||||
|
|
||||||
const timekeeping = () => {
|
|
||||||
clearInterval(timer)
|
|
||||||
if (route.name == 'login' || isLock.value) return
|
|
||||||
// 设置不锁屏
|
|
||||||
useLockscreen.setLock(false)
|
|
||||||
// 重置锁屏时间
|
|
||||||
useLockscreen.setLockTime()
|
|
||||||
timer = setInterval(() => {
|
|
||||||
// 锁屏倒计时递减
|
|
||||||
useLockscreen.setLockTime(lockTime.value - 1)
|
|
||||||
if (lockTime.value <= 0) {
|
|
||||||
// 设置锁屏
|
|
||||||
useLockscreen.setLock(true)
|
|
||||||
return clearInterval(timer)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('mousedown', timekeeping)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('mousedown', timekeeping)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type import('naive-ui').GlobalThemeOverrides
|
||||||
|
*/
|
||||||
|
const getThemeOverrides = computed(() => {
|
||||||
|
const appTheme = designStore.appTheme;
|
||||||
|
const lightenStr = lighten(designStore.appTheme, 6);
|
||||||
return {
|
return {
|
||||||
darkTheme: createTheme([inputDark, datePickerDark]),
|
common: {
|
||||||
getDarkTheme,
|
primaryColor: appTheme,
|
||||||
zhCN,
|
primaryColorHover: lightenStr,
|
||||||
dateZhCN,
|
primaryColorPressed: lightenStr,
|
||||||
isLock,
|
primaryColorSuppl: appTheme,
|
||||||
getThemeOverrides
|
},
|
||||||
}
|
LoadingBar: {
|
||||||
}
|
colorLoading: appTheme,
|
||||||
})
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
|
||||||
|
|
||||||
|
let timer: NodeJS.Timer;
|
||||||
|
|
||||||
|
const timekeeping = () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
if (route.name == 'login' || isLock.value) return;
|
||||||
|
// 设置不锁屏
|
||||||
|
useScreenLock.setLock(false);
|
||||||
|
// 重置锁屏时间
|
||||||
|
useScreenLock.setLockTime();
|
||||||
|
timer = setInterval(() => {
|
||||||
|
// 锁屏倒计时递减
|
||||||
|
useScreenLock.setLockTime(lockTime.value - 1);
|
||||||
|
if (lockTime.value <= 0) {
|
||||||
|
// 设置锁屏
|
||||||
|
useScreenLock.setLock(true);
|
||||||
|
return clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousedown', timekeeping);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousedown', timekeeping);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
@import 'styles/global.less';
|
|
||||||
@import 'styles/common.less';
|
|
||||||
@import 'styles/override.less';
|
|
||||||
|
|
||||||
.slide-up-enter-active,
|
|
||||||
.slide-up-leave-active {
|
|
||||||
transition: transform 0.35s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-up-enter-form,
|
|
||||||
.slide-up-leave-to {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
import http from '@/utils/http/axios'
|
import { Alova } from '@/utils/http/alova/index';
|
||||||
|
|
||||||
|
export interface TypeVisits {
|
||||||
|
dayVisits: number;
|
||||||
|
rise: number;
|
||||||
|
decline: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
export interface TypeSaleroom {
|
||||||
|
weekSaleroom: number;
|
||||||
|
amount: number;
|
||||||
|
degree: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypeOrderLarge {
|
||||||
|
weekLarge: number;
|
||||||
|
rise: number;
|
||||||
|
decline: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypeConsole {
|
||||||
|
visits: TypeVisits;
|
||||||
|
//销售额
|
||||||
|
saleroom: TypeSaleroom;
|
||||||
|
//订单量
|
||||||
|
orderLarge: TypeOrderLarge;
|
||||||
|
//成交额度
|
||||||
|
volume: TypeOrderLarge;
|
||||||
|
}
|
||||||
|
|
||||||
//获取主控台信息
|
//获取主控台信息
|
||||||
export function getConsoleInfo() {
|
export function getConsoleInfo() {
|
||||||
return http.request(
|
return Alova.Get<TypeConsole>('/dashboard/console');
|
||||||
{
|
|
||||||
url: '/dashboard/console',
|
|
||||||
method: 'get'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import http from '@/utils/http/axios'
|
import { Alova } from '@/utils/http/alova/index';
|
||||||
|
export interface ListDate {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
type: number;
|
||||||
|
subtitle: string;
|
||||||
|
openType: number;
|
||||||
|
auth: string;
|
||||||
|
path: string;
|
||||||
|
children?: ListDate[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 根据用户id获取用户菜单
|
* @description: 根据用户id获取用户菜单
|
||||||
*/
|
*/
|
||||||
export function adminMenus() {
|
export function adminMenus() {
|
||||||
return http.request({
|
return Alova.Get('/menus');
|
||||||
url: '/menus',
|
|
||||||
method: 'GET'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取tree菜单列表
|
* 获取tree菜单列表
|
||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
export function getMenuList(params) {
|
export function getMenuList(params?) {
|
||||||
return http.request({
|
return Alova.Get<{ list: ListDate[] }>('/menu/list', {
|
||||||
url: '/menu/list',
|
params,
|
||||||
method: 'GET',
|
});
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import http from '@/utils/http/axios'
|
import { Alova } from '@/utils/http/alova/index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 角色列表
|
* @description: 角色列表
|
||||||
*/
|
*/
|
||||||
export function getRoleList() {
|
export function getRoleList(params) {
|
||||||
return http.request({
|
return Alova.Get('/role/list', { params });
|
||||||
url: '/role/list',
|
}
|
||||||
method: 'GET'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +1,45 @@
|
|||||||
import http from '@/utils/http/axios'
|
import { Alova } from '@/utils/http/alova/index';
|
||||||
|
|
||||||
export interface BasicResponseModel<T = any> {
|
|
||||||
code: number
|
|
||||||
message: string
|
|
||||||
result: T
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BasicPageParams {
|
|
||||||
pageNumber: number
|
|
||||||
pageSize: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 获取用户信息
|
* @description: 获取用户信息
|
||||||
*/
|
*/
|
||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return http.request(
|
return Alova.Get<InResult>('/admin_info', {
|
||||||
{
|
meta: {
|
||||||
url: '/admin_info',
|
isReturnNativeResponse: true,
|
||||||
method: 'get'
|
},
|
||||||
}
|
});
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 用户登录
|
* @description: 用户登录
|
||||||
*/
|
*/
|
||||||
export function login(params) {
|
export function login(params) {
|
||||||
return http.request<BasicResponseModel>(
|
return Alova.Post<InResult>(
|
||||||
{
|
'/login',
|
||||||
url: '/login',
|
{
|
||||||
method: 'POST',
|
params,
|
||||||
params
|
},
|
||||||
},
|
{
|
||||||
{
|
meta: {
|
||||||
isTransformRequestResult: false
|
isReturnNativeResponse: true,
|
||||||
}
|
},
|
||||||
)
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 用户修改密码
|
* @description: 用户修改密码
|
||||||
*/
|
*/
|
||||||
export function changePassword(params, uid) {
|
export function changePassword(params, uid) {
|
||||||
return http.request(
|
return Alova.Post(`/user/u${uid}/changepw`, { params });
|
||||||
{
|
|
||||||
url: `/user/u${ uid }/changepw`,
|
|
||||||
method: 'POST',
|
|
||||||
params
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isTransformRequestResult: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 用户登出
|
* @description: 用户登出
|
||||||
*/
|
*/
|
||||||
export function logout(params) {
|
export function logout(params) {
|
||||||
return http.request({
|
return Alova.Post('/login/logout', {
|
||||||
url: '/login/logout',
|
params,
|
||||||
method: 'POST',
|
});
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import http from '@/utils/http/axios'
|
import { Alova } from '@/utils/http/alova/index';
|
||||||
|
|
||||||
//获取table
|
//获取table
|
||||||
export function getTableList(params) {
|
export function getTableList(params) {
|
||||||
return http.request(
|
return Alova.Get('/table/list', { params });
|
||||||
{
|
|
||||||
url: '/table/list',
|
|
||||||
method: 'get',
|
|
||||||
params
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 6.3 KiB |
15
src/assets/images/nav-horizontal-mix.svg
Normal file
15
src/assets/images/nav-horizontal-mix.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="52px" height="45px" viewBox="0 0 52 45" enable-background="new 0 0 52 45" xml:space="preserve"> <image id="image0" width="52" height="45" x="0" y="0"
|
||||||
|
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAAtCAMAAADWf7iKAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
|
||||||
|
AAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAAdVBMVEX///8AAABkbnc9RVY7
|
||||||
|
QE9fY3GIiJZjbHk9QFOCi5QAAAA9QlZfZHMAAAA4QFEAAADt7/Lf5OTt7/KChoYAAADu8PTf3+Nw
|
||||||
|
c3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyNUkyOEn////w8vWhURXFAAAAI3RS
|
||||||
|
TlMAAE/2/uZJUP45AvfkBP4F9LrwPwP0vUkOAREsNjk0JwYHCLrjEiIAAAABYktHRACIBR1IAAAA
|
||||||
|
CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5QgGAhE5kB5L+gAAAIZJREFUSMft1rsSgjAQhWEW
|
||||||
|
FTUYIopKlPui7/+IZqFhbMxmhm7//qvPiaKgAOIN+rbdJQCE9qO3cR2OhFTKMYgn5ZDmGcy0Q4aJ
|
||||||
|
0AhaoPdPnz8JEiRIkKCV0DkE5YQ0D12uBQ01B93uj9LSJdDPV1V71rRlMf0Iq7p+8Kw3aj4fAJYR
|
||||||
|
TCigL0lMJ5P4y7LRAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA4LTA2VDAyOjE3OjU2KzAwOjAw
|
||||||
|
Kbo8/wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wOC0wNlQwMjoxNzo1NiswMDowMFjnhEMAAAAA
|
||||||
|
SUVORK5CYII=" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,25 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<DialogContent/>
|
|
||||||
<n-notification-provider>
|
<n-notification-provider>
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<MessageContent/>
|
<slot name="default"></slot>
|
||||||
<slot slot="default"></slot>
|
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-notification-provider>
|
</n-notification-provider>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue';
|
||||||
import { MessageContent } from '@/components/MessageContent'
|
import { NDialogProvider, NNotificationProvider, NMessageProvider } from 'naive-ui';
|
||||||
import { DialogContent } from '@/components/DialogContent'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Application',
|
name: 'Application',
|
||||||
components: { MessageContent, DialogContent },
|
components: {
|
||||||
setup() {
|
NDialogProvider,
|
||||||
return {}
|
NNotificationProvider,
|
||||||
}
|
NMessageProvider,
|
||||||
})
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import AppProvider from './Application.vue'
|
import AppProvider from './Application.vue';
|
||||||
|
|
||||||
export { AppProvider }
|
export { AppProvider };
|
||||||
|
|||||||
@@ -3,108 +3,107 @@
|
|||||||
{{ value }}
|
{{ value }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
|
import { ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
|
||||||
import { useTransition, TransitionPresets } from '@vueuse/core';
|
import { useTransition, TransitionPresets } from '@vueuse/core';
|
||||||
import { isNumber } from '@/utils/is';
|
import { isNumber } from '@/utils/is';
|
||||||
|
|
||||||
const props = {
|
defineOptions({ name: 'CountTo' });
|
||||||
startVal: { type: Number, default: 0 },
|
|
||||||
endVal: { type: Number, default: 2021 },
|
const props = defineProps({
|
||||||
duration: { type: Number, default: 1500 },
|
startVal: { type: Number, default: 0 },
|
||||||
autoplay: { type: Boolean, default: true },
|
endVal: { type: Number, default: 2021 },
|
||||||
decimals: {
|
duration: { type: Number, default: 1500 },
|
||||||
type: Number,
|
autoplay: { type: Boolean, default: true },
|
||||||
default: 0,
|
decimals: {
|
||||||
validator(value: number) {
|
type: Number,
|
||||||
return value >= 0;
|
default: 0,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
prefix: { type: String, default: '' },
|
||||||
prefix: { type: String, default: '' },
|
suffix: { type: String, default: '' },
|
||||||
suffix: { type: String, default: '' },
|
separator: { type: String, default: ',' },
|
||||||
separator: { type: String, default: ',' },
|
decimal: { type: String, default: '.' },
|
||||||
decimal: { type: String, default: '.' },
|
/**
|
||||||
/**
|
* font color
|
||||||
* font color
|
*/
|
||||||
*/
|
color: { type: String },
|
||||||
color: { type: String },
|
/**
|
||||||
/**
|
* Turn on digital animation
|
||||||
* Turn on digital animation
|
*/
|
||||||
*/
|
useEasing: { type: Boolean, default: true },
|
||||||
useEasing: { type: Boolean, default: true },
|
/**
|
||||||
/**
|
* Digital animation
|
||||||
* Digital animation
|
*/
|
||||||
*/
|
transition: { type: String, default: 'linear' },
|
||||||
transition: { type: String, default: 'linear' },
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
const emit = defineEmits(['onStarted', 'onFinished']);
|
||||||
name: 'CountTo',
|
|
||||||
props,
|
|
||||||
emits: ['onStarted', 'onFinished'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const source = ref(props.startVal);
|
|
||||||
const disabled = ref(false);
|
|
||||||
let outputValue = useTransition(source);
|
|
||||||
|
|
||||||
const value = computed(() => formatNumber(unref(outputValue)));
|
const source = ref(props.startVal);
|
||||||
|
const disabled = ref(false);
|
||||||
|
let outputValue = useTransition(source);
|
||||||
|
|
||||||
watchEffect(() => {
|
const value = computed(() => formatNumber(unref(outputValue)));
|
||||||
source.value = props.startVal;
|
|
||||||
|
watchEffect(() => {
|
||||||
|
source.value = props.startVal;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => props.startVal, () => props.endVal], () => {
|
||||||
|
if (props.autoplay) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.autoplay && start();
|
||||||
|
});
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
run();
|
||||||
|
source.value = props.endVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
source.value = props.startVal;
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
outputValue = useTransition(source, {
|
||||||
|
disabled,
|
||||||
|
duration: props.duration,
|
||||||
|
onFinished: () => emit('onFinished'),
|
||||||
|
onStarted: () => emit('onStarted'),
|
||||||
|
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
watch([() => props.startVal, () => props.endVal], () => {
|
function formatNumber(num: number | string) {
|
||||||
if (props.autoplay) {
|
if (!num && num !== 0) {
|
||||||
start();
|
return '';
|
||||||
|
}
|
||||||
|
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||||
|
num = Number(num).toFixed(decimals);
|
||||||
|
num += '';
|
||||||
|
|
||||||
|
const x = num.split('.');
|
||||||
|
let x1 = x[0];
|
||||||
|
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||||
|
|
||||||
|
const rgx = /(\d+)(\d{3})/;
|
||||||
|
if (separator && !isNumber(separator)) {
|
||||||
|
while (rgx.test(x1)) {
|
||||||
|
x1 = x1.replace(rgx, '$1' + separator + '$2');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
props.autoplay && start();
|
|
||||||
});
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
run();
|
|
||||||
source.value = props.endVal;
|
|
||||||
}
|
}
|
||||||
|
return prefix + x1 + x2 + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
function reset() {
|
defineExpose({
|
||||||
source.value = props.startVal;
|
reset,
|
||||||
run();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
outputValue = useTransition(source, {
|
|
||||||
disabled,
|
|
||||||
duration: props.duration,
|
|
||||||
onFinished: () => emit('onFinished'),
|
|
||||||
onStarted: () => emit('onStarted'),
|
|
||||||
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(num: number | string) {
|
|
||||||
if (!num) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
|
||||||
num = Number(num).toFixed(decimals);
|
|
||||||
num += '';
|
|
||||||
|
|
||||||
const x = num.split('.');
|
|
||||||
let x1 = x[0];
|
|
||||||
const x2 = x.length > 1 ? decimal + x[1] : '';
|
|
||||||
|
|
||||||
const rgx = /(\d+)(\d{3})/;
|
|
||||||
if (separator && !isNumber(separator)) {
|
|
||||||
while (rgx.test(x1)) {
|
|
||||||
x1 = x1.replace(rgx, '$1' + separator + '$2');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefix + x1 + x2 + suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value, start, reset };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import DialogContent from './index.vue'
|
|
||||||
|
|
||||||
export { DialogContent }
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<template></template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { useDialog } from 'naive-ui'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'DialogContent',
|
|
||||||
setup() {
|
|
||||||
//挂载在 window 方便与在js中使用
|
|
||||||
window.$dialog = useDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
4
src/components/Form/index.ts
Normal file
4
src/components/Form/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as BasicForm } from './src/BasicForm.vue';
|
||||||
|
export { useForm } from './src/hooks/useForm';
|
||||||
|
export * from './src/types/form';
|
||||||
|
export * from './src/types/index';
|
||||||
319
src/components/Form/src/BasicForm.vue
Normal file
319
src/components/Form/src/BasicForm.vue
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
<template>
|
||||||
|
<n-form v-bind="getBindValue" :model="formModel" ref="formElRef">
|
||||||
|
<n-grid v-bind="getGrid">
|
||||||
|
<n-gi v-bind="schema.giProps" v-for="schema in getSchema" :key="schema.field">
|
||||||
|
<n-form-item :label="schema.label" :path="schema.field">
|
||||||
|
<!--标签名右侧温馨提示-->
|
||||||
|
<template #label v-if="schema.labelMessage">
|
||||||
|
{{ schema.label }}
|
||||||
|
<n-tooltip trigger="hover" :style="schema.labelMessageStyle">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon size="18" class="text-gray-400 cursor-pointer">
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ schema.labelMessage }}
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--判断插槽-->
|
||||||
|
<template v-if="schema.slot">
|
||||||
|
<slot
|
||||||
|
:name="schema.slot"
|
||||||
|
:model="formModel"
|
||||||
|
:field="schema.field"
|
||||||
|
:value="formModel[schema.field]"
|
||||||
|
></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--NCheckbox-->
|
||||||
|
<template v-else-if="schema.component === 'NCheckbox'">
|
||||||
|
<n-checkbox-group v-model:value="formModel[schema.field]">
|
||||||
|
<n-space>
|
||||||
|
<n-checkbox
|
||||||
|
v-for="item in schema.componentProps.options"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</n-checkbox-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--NRadioGroup-->
|
||||||
|
<template v-else-if="schema.component === 'NRadioGroup'">
|
||||||
|
<n-radio-group v-model:value="formModel[schema.field]">
|
||||||
|
<n-space>
|
||||||
|
<n-radio
|
||||||
|
v-for="item in schema.componentProps.options"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</n-radio>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</template>
|
||||||
|
<!--动态渲染表单组件-->
|
||||||
|
<component
|
||||||
|
v-else
|
||||||
|
v-bind="getComponentProps(schema)"
|
||||||
|
:is="schema.component"
|
||||||
|
v-model:value="formModel[schema.field]"
|
||||||
|
:class="{ isFull: schema.isFull != false && getProps.isFull }"
|
||||||
|
/>
|
||||||
|
<!--组件后面的内容-->
|
||||||
|
<template v-if="schema.suffix">
|
||||||
|
<slot
|
||||||
|
:name="schema.suffix"
|
||||||
|
:model="formModel"
|
||||||
|
:field="schema.field"
|
||||||
|
:value="formModel[schema.field]"
|
||||||
|
></slot>
|
||||||
|
</template>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<!--提交 重置 展开 收起 按钮-->
|
||||||
|
<n-gi
|
||||||
|
:span="isInline ? '' : 24"
|
||||||
|
:suffix="isInline ? true : false"
|
||||||
|
#="{ overflow }"
|
||||||
|
v-if="getProps.showActionButtonGroup"
|
||||||
|
>
|
||||||
|
<n-space
|
||||||
|
align="center"
|
||||||
|
:justify="isInline ? 'end' : 'start'"
|
||||||
|
:style="{ 'margin-left': `${isInline ? 12 : getProps.labelWidth}px` }"
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
v-if="getProps.showSubmitButton"
|
||||||
|
v-bind="getSubmitBtnOptions"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="loadingSub"
|
||||||
|
attr-type="submit"
|
||||||
|
>{{ getProps.submitButtonText }}</n-button
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
v-if="getProps.showResetButton"
|
||||||
|
v-bind="getResetBtnOptions"
|
||||||
|
@click="resetFields"
|
||||||
|
>{{ getProps.resetButtonText }}</n-button
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
text
|
||||||
|
icon-placement="right"
|
||||||
|
v-if="isInline && getProps.showAdvancedButton"
|
||||||
|
@click="unfoldToggle"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="14" class="unfold-icon" v-if="overflow">
|
||||||
|
<DownOutlined />
|
||||||
|
</n-icon>
|
||||||
|
<n-icon size="14" class="unfold-icon" v-else>
|
||||||
|
<UpOutlined />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ overflow ? '展开' : '收起' }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, computed, unref, onMounted, watch } from 'vue';
|
||||||
|
import { createPlaceholderMessage } from './helper';
|
||||||
|
import { useFormEvents } from './hooks/useFormEvents';
|
||||||
|
import { useFormValues } from './hooks/useFormValues';
|
||||||
|
|
||||||
|
import { basicProps } from './props';
|
||||||
|
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { GridProps } from 'naive-ui/lib/grid';
|
||||||
|
import type { FormSchema, FormProps, FormActionType } from './types/form';
|
||||||
|
|
||||||
|
import { isArray } from '@/utils/is/index';
|
||||||
|
import { deepMerge } from '@/utils';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicForm',
|
||||||
|
components: { DownOutlined, UpOutlined, QuestionCircleOutlined },
|
||||||
|
props: {
|
||||||
|
...basicProps,
|
||||||
|
},
|
||||||
|
emits: ['reset', 'submit', 'register'],
|
||||||
|
setup(props, { emit, attrs }) {
|
||||||
|
const defaultFormModel = ref<Recordable>({});
|
||||||
|
const formModel = reactive<Recordable>({});
|
||||||
|
const propsRef = ref<Partial<FormProps>>({});
|
||||||
|
const schemaRef = ref<Nullable<FormSchema[]>>(null);
|
||||||
|
const formElRef = ref<Nullable<FormActionType>>(null);
|
||||||
|
const gridCollapsed = ref(true);
|
||||||
|
const loadingSub = ref(false);
|
||||||
|
const isUpdateDefaultRef = ref(false);
|
||||||
|
|
||||||
|
const getSubmitBtnOptions = computed(() => {
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
size: props.size,
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
props.submitButtonOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getResetBtnOptions = computed(() => {
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
size: props.size,
|
||||||
|
type: 'default',
|
||||||
|
},
|
||||||
|
props.resetButtonOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getComponentProps(schema) {
|
||||||
|
const compProps = schema.componentProps ?? {};
|
||||||
|
const component = schema.component;
|
||||||
|
return {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: createPlaceholderMessage(unref(component)),
|
||||||
|
...compProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProps = computed((): FormProps => {
|
||||||
|
const formProps = { ...props, ...unref(propsRef) } as FormProps;
|
||||||
|
const rulesObj: any = {
|
||||||
|
rules: {},
|
||||||
|
};
|
||||||
|
const schemas: any = formProps.schemas || [];
|
||||||
|
schemas.forEach((item) => {
|
||||||
|
if (item.rules && isArray(item.rules)) {
|
||||||
|
rulesObj.rules[item.field] = item.rules;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ...formProps, ...unref(rulesObj) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const isInline = computed(() => {
|
||||||
|
const { layout } = unref(getProps);
|
||||||
|
return layout === 'inline';
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGrid = computed((): GridProps => {
|
||||||
|
const { gridProps } = unref(getProps);
|
||||||
|
return {
|
||||||
|
...gridProps,
|
||||||
|
collapsed: isInline.value ? gridCollapsed.value : false,
|
||||||
|
responsive: 'screen',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBindValue = computed(
|
||||||
|
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable)
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSchema = computed((): FormSchema[] => {
|
||||||
|
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
|
||||||
|
for (const schema of schemas) {
|
||||||
|
const { defaultValue } = schema;
|
||||||
|
// handle date type
|
||||||
|
// dateItemType.includes(component as string)
|
||||||
|
if (defaultValue) {
|
||||||
|
schema.defaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schemas as FormSchema[];
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleFormValues, initDefault } = useFormValues({
|
||||||
|
defaultFormModel,
|
||||||
|
getSchema,
|
||||||
|
formModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, validate, resetFields, getFieldsValue, clearValidate, setFieldsValue } =
|
||||||
|
useFormEvents({
|
||||||
|
emit,
|
||||||
|
getProps,
|
||||||
|
formModel,
|
||||||
|
getSchema,
|
||||||
|
formElRef: formElRef as Ref<FormActionType>,
|
||||||
|
defaultFormModel,
|
||||||
|
loadingSub,
|
||||||
|
handleFormValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
function unfoldToggle() {
|
||||||
|
gridCollapsed.value = !gridCollapsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setProps(formProps: Partial<FormProps>): Promise<void> {
|
||||||
|
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formActionType: Partial<FormActionType> = {
|
||||||
|
getFieldsValue,
|
||||||
|
setFieldsValue,
|
||||||
|
resetFields,
|
||||||
|
validate,
|
||||||
|
clearValidate,
|
||||||
|
setProps,
|
||||||
|
submit: handleSubmit,
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => getSchema.value,
|
||||||
|
(schema) => {
|
||||||
|
if (unref(isUpdateDefaultRef)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (schema?.length) {
|
||||||
|
initDefault();
|
||||||
|
isUpdateDefaultRef.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initDefault();
|
||||||
|
emit('register', formActionType);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
formElRef,
|
||||||
|
formModel,
|
||||||
|
getGrid,
|
||||||
|
getProps,
|
||||||
|
getBindValue,
|
||||||
|
getSchema,
|
||||||
|
getSubmitBtnOptions,
|
||||||
|
getResetBtnOptions,
|
||||||
|
handleSubmit,
|
||||||
|
resetFields,
|
||||||
|
loadingSub,
|
||||||
|
isInline,
|
||||||
|
getComponentProps,
|
||||||
|
unfoldToggle,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.isFull {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unfold-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: -3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
src/components/Form/src/helper.ts
Normal file
42
src/components/Form/src/helper.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ComponentType } from './types/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 生成placeholder
|
||||||
|
*/
|
||||||
|
export function createPlaceholderMessage(component: ComponentType) {
|
||||||
|
if (component === 'NInput') return '请输入';
|
||||||
|
if (
|
||||||
|
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
|
||||||
|
component
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return '请选择';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
|
||||||
|
|
||||||
|
function genType() {
|
||||||
|
return [...DATE_TYPE, 'RangePicker'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间字段
|
||||||
|
*/
|
||||||
|
export const dateItemType = genType();
|
||||||
|
|
||||||
|
export function defaultType(component) {
|
||||||
|
if (component === 'NInput') return '';
|
||||||
|
if (component === 'NInputNumber') return null;
|
||||||
|
return [
|
||||||
|
'NPicker',
|
||||||
|
'NSelect',
|
||||||
|
'NCheckbox',
|
||||||
|
'NRadio',
|
||||||
|
'NSwitch',
|
||||||
|
'NDatePicker',
|
||||||
|
'NTimePicker',
|
||||||
|
].includes(component)
|
||||||
|
? ''
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
95
src/components/Form/src/hooks/useForm.ts
Normal file
95
src/components/Form/src/hooks/useForm.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
|
||||||
|
import type { DynamicProps } from '/#/utils';
|
||||||
|
|
||||||
|
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
|
||||||
|
import { isProdMode } from '@/utils/env';
|
||||||
|
import { getDynamicProps } from '@/utils';
|
||||||
|
|
||||||
|
type Props = Partial<DynamicProps<FormProps>>;
|
||||||
|
|
||||||
|
export function useForm(props?: Props): UseFormReturnType {
|
||||||
|
const formRef = ref<Nullable<FormActionType>>(null);
|
||||||
|
const loadedRef = ref<Nullable<boolean>>(false);
|
||||||
|
|
||||||
|
async function getForm() {
|
||||||
|
const form = unref(formRef);
|
||||||
|
if (!form) {
|
||||||
|
console.error(
|
||||||
|
'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
return form as FormActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function register(instance: FormActionType) {
|
||||||
|
isProdMode() &&
|
||||||
|
onUnmounted(() => {
|
||||||
|
formRef.value = null;
|
||||||
|
loadedRef.value = null;
|
||||||
|
});
|
||||||
|
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
|
||||||
|
|
||||||
|
formRef.value = instance;
|
||||||
|
loadedRef.value = true;
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props,
|
||||||
|
() => {
|
||||||
|
props && instance.setProps(getDynamicProps(props));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods: FormActionType = {
|
||||||
|
setProps: async (formProps: Partial<FormProps>) => {
|
||||||
|
const form = await getForm();
|
||||||
|
await form.setProps(formProps);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetFields: async () => {
|
||||||
|
getForm().then(async (form) => {
|
||||||
|
await form.resetFields();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearValidate: async (name?: string | string[]) => {
|
||||||
|
const form = await getForm();
|
||||||
|
await form.clearValidate(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFieldsValue: <T>() => {
|
||||||
|
return unref(formRef)?.getFieldsValue() as T;
|
||||||
|
},
|
||||||
|
|
||||||
|
setFieldsValue: async <T>(values: T) => {
|
||||||
|
const form = await getForm();
|
||||||
|
await form.setFieldsValue<T>(values);
|
||||||
|
},
|
||||||
|
|
||||||
|
submit: async (): Promise<any> => {
|
||||||
|
const form = await getForm();
|
||||||
|
return form.submit();
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: async (nameList?: any[]): Promise<Recordable> => {
|
||||||
|
const form = await getForm();
|
||||||
|
return form.validate(nameList);
|
||||||
|
},
|
||||||
|
|
||||||
|
setLoading: (value: boolean) => {
|
||||||
|
loadedRef.value = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSchema: async (values) => {
|
||||||
|
const form = await getForm();
|
||||||
|
form.setSchema(values);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [register, methods];
|
||||||
|
}
|
||||||
11
src/components/Form/src/hooks/useFormContext.ts
Normal file
11
src/components/Form/src/hooks/useFormContext.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { provide, inject } from 'vue';
|
||||||
|
|
||||||
|
const key = Symbol('formElRef');
|
||||||
|
|
||||||
|
export function createFormContext(instance) {
|
||||||
|
provide(key, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFormContext() {
|
||||||
|
return inject(key);
|
||||||
|
}
|
||||||
116
src/components/Form/src/hooks/useFormEvents.ts
Normal file
116
src/components/Form/src/hooks/useFormEvents.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
import type { FormProps, FormSchema, FormActionType } from '../types/form';
|
||||||
|
import { unref, toRaw } from 'vue';
|
||||||
|
import { isFunction } from '@/utils/is';
|
||||||
|
|
||||||
|
declare type EmitType = (event: string, ...args: any[]) => void;
|
||||||
|
|
||||||
|
interface UseFormActionContext {
|
||||||
|
emit: EmitType;
|
||||||
|
getProps: ComputedRef<FormProps>;
|
||||||
|
getSchema: ComputedRef<FormSchema[]>;
|
||||||
|
formModel: Recordable;
|
||||||
|
formElRef: Ref<FormActionType>;
|
||||||
|
defaultFormModel: Recordable;
|
||||||
|
loadingSub: Ref<boolean>;
|
||||||
|
handleFormValues: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFormEvents({
|
||||||
|
emit,
|
||||||
|
getProps,
|
||||||
|
formModel,
|
||||||
|
getSchema,
|
||||||
|
formElRef,
|
||||||
|
defaultFormModel,
|
||||||
|
loadingSub,
|
||||||
|
handleFormValues,
|
||||||
|
}: UseFormActionContext) {
|
||||||
|
// 验证
|
||||||
|
async function validate() {
|
||||||
|
return unref(formElRef)?.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
async function handleSubmit(e?: Event): Promise<object | boolean> {
|
||||||
|
e && e.preventDefault();
|
||||||
|
loadingSub.value = true;
|
||||||
|
const { submitFunc } = unref(getProps);
|
||||||
|
if (submitFunc && isFunction(submitFunc)) {
|
||||||
|
await submitFunc();
|
||||||
|
loadingSub.value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const formEl = unref(formElRef);
|
||||||
|
if (!formEl) return false;
|
||||||
|
try {
|
||||||
|
await validate();
|
||||||
|
const values = getFieldsValue();
|
||||||
|
loadingSub.value = false;
|
||||||
|
emit('submit', values);
|
||||||
|
return values;
|
||||||
|
} catch (error: any) {
|
||||||
|
emit('submit', false);
|
||||||
|
loadingSub.value = false;
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//清空校验
|
||||||
|
async function clearValidate() {
|
||||||
|
// @ts-ignore
|
||||||
|
await unref(formElRef)?.restoreValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
//重置
|
||||||
|
async function resetFields(): Promise<void> {
|
||||||
|
const { resetFunc, submitOnReset } = unref(getProps);
|
||||||
|
resetFunc && isFunction(resetFunc) && (await resetFunc());
|
||||||
|
|
||||||
|
const formEl = unref(formElRef);
|
||||||
|
if (!formEl) return;
|
||||||
|
Object.keys(formModel).forEach((key) => {
|
||||||
|
formModel[key] = unref(defaultFormModel)[key] || null;
|
||||||
|
});
|
||||||
|
await clearValidate();
|
||||||
|
const fromValues = handleFormValues(toRaw(unref(formModel)));
|
||||||
|
emit('reset', fromValues);
|
||||||
|
submitOnReset && (await handleSubmit());
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取表单值
|
||||||
|
function getFieldsValue(): Recordable {
|
||||||
|
const formEl = unref(formElRef);
|
||||||
|
if (!formEl) return {};
|
||||||
|
return handleFormValues(toRaw(unref(formModel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
//设置表单字段值
|
||||||
|
async function setFieldsValue(values: Recordable): Promise<void> {
|
||||||
|
const fields = unref(getSchema)
|
||||||
|
.map((item) => item.field)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
const value = values[key];
|
||||||
|
if (fields.includes(key)) {
|
||||||
|
formModel[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoading(value: boolean): void {
|
||||||
|
loadingSub.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSubmit,
|
||||||
|
validate,
|
||||||
|
resetFields,
|
||||||
|
getFieldsValue,
|
||||||
|
clearValidate,
|
||||||
|
setFieldsValue,
|
||||||
|
setLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
54
src/components/Form/src/hooks/useFormValues.ts
Normal file
54
src/components/Form/src/hooks/useFormValues.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is';
|
||||||
|
import { unref } from 'vue';
|
||||||
|
import type { Ref, ComputedRef } from 'vue';
|
||||||
|
import type { FormSchema } from '../types/form';
|
||||||
|
import { set } from 'lodash-es';
|
||||||
|
|
||||||
|
interface UseFormValuesContext {
|
||||||
|
defaultFormModel: Ref<any>;
|
||||||
|
getSchema: ComputedRef<FormSchema[]>;
|
||||||
|
formModel: Recordable;
|
||||||
|
}
|
||||||
|
export function useFormValues({ defaultFormModel, getSchema, formModel }: UseFormValuesContext) {
|
||||||
|
// 加工 form values
|
||||||
|
function handleFormValues(values: Recordable) {
|
||||||
|
if (!isObject(values)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const res: Recordable = {};
|
||||||
|
for (const item of Object.entries(values)) {
|
||||||
|
let [, value] = item;
|
||||||
|
const [key] = item;
|
||||||
|
if (
|
||||||
|
!key ||
|
||||||
|
(isArray(value) && value.length === 0) ||
|
||||||
|
isFunction(value) ||
|
||||||
|
isNullOrUnDef(value)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 删除空格
|
||||||
|
if (isString(value)) {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
set(res, key, value);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
//初始化默认值
|
||||||
|
function initDefault() {
|
||||||
|
const schemas = unref(getSchema);
|
||||||
|
const obj: Recordable = {};
|
||||||
|
schemas.forEach((item) => {
|
||||||
|
const { defaultValue } = item;
|
||||||
|
if (!isNullOrUnDef(defaultValue)) {
|
||||||
|
obj[item.field] = defaultValue;
|
||||||
|
formModel[item.field] = defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
defaultFormModel.value = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { handleFormValues, initDefault };
|
||||||
|
}
|
||||||
82
src/components/Form/src/props.ts
Normal file
82
src/components/Form/src/props.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { CSSProperties, PropType } from 'vue';
|
||||||
|
import { FormSchema } from './types/form';
|
||||||
|
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
|
||||||
|
import type { ButtonProps } from 'naive-ui/lib/button';
|
||||||
|
import { propTypes } from '@/utils/propTypes';
|
||||||
|
export const basicProps = {
|
||||||
|
// 标签宽度 固定宽度
|
||||||
|
labelWidth: {
|
||||||
|
type: [Number, String] as PropType<number | string>,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
// 表单配置规则
|
||||||
|
schemas: {
|
||||||
|
type: [Array] as PropType<FormSchema[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
//布局方式
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: 'inline',
|
||||||
|
},
|
||||||
|
//是否展示为行内表单
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
//大小
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'medium',
|
||||||
|
},
|
||||||
|
//标签位置
|
||||||
|
labelPlacement: {
|
||||||
|
type: String,
|
||||||
|
default: 'left',
|
||||||
|
},
|
||||||
|
//组件是否width 100%
|
||||||
|
isFull: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
//是否显示操作按钮(查询/重置)
|
||||||
|
showActionButtonGroup: propTypes.bool.def(true),
|
||||||
|
// 显示重置按钮
|
||||||
|
showResetButton: propTypes.bool.def(true),
|
||||||
|
//重置按钮配置
|
||||||
|
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
|
||||||
|
// 显示确认按钮
|
||||||
|
showSubmitButton: propTypes.bool.def(true),
|
||||||
|
// 确认按钮配置
|
||||||
|
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
|
||||||
|
//展开收起按钮
|
||||||
|
showAdvancedButton: propTypes.bool.def(true),
|
||||||
|
// 确认按钮文字
|
||||||
|
submitButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: '查询',
|
||||||
|
},
|
||||||
|
//重置按钮文字
|
||||||
|
resetButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: '重置',
|
||||||
|
},
|
||||||
|
//grid 配置
|
||||||
|
gridProps: Object as PropType<GridProps>,
|
||||||
|
//gi配置
|
||||||
|
giProps: Object as PropType<GridItemProps>,
|
||||||
|
//grid 样式
|
||||||
|
baseGridStyle: {
|
||||||
|
type: Object as PropType<CSSProperties>,
|
||||||
|
},
|
||||||
|
//是否折叠
|
||||||
|
collapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
//默认展示的行数
|
||||||
|
collapsedRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
61
src/components/Form/src/types/form.ts
Normal file
61
src/components/Form/src/types/form.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentType } from './index';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
|
||||||
|
import type { ButtonProps } from 'naive-ui/lib/button';
|
||||||
|
|
||||||
|
export interface FormSchema {
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
labelMessage?: string;
|
||||||
|
labelMessageStyle?: object | string;
|
||||||
|
defaultValue?: any;
|
||||||
|
component?: ComponentType;
|
||||||
|
componentProps?: object;
|
||||||
|
slot?: string;
|
||||||
|
rules?: object | object[];
|
||||||
|
giProps?: GridItemProps;
|
||||||
|
isFull?: boolean;
|
||||||
|
suffix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormProps {
|
||||||
|
model?: Recordable;
|
||||||
|
labelWidth?: number | string;
|
||||||
|
schemas?: FormSchema[];
|
||||||
|
inline: boolean;
|
||||||
|
layout?: string;
|
||||||
|
size: string;
|
||||||
|
labelPlacement: string;
|
||||||
|
isFull: boolean;
|
||||||
|
showActionButtonGroup?: boolean;
|
||||||
|
showResetButton?: boolean;
|
||||||
|
resetButtonOptions?: Partial<ButtonProps>;
|
||||||
|
showSubmitButton?: boolean;
|
||||||
|
showAdvancedButton?: boolean;
|
||||||
|
submitButtonOptions?: Partial<ButtonProps>;
|
||||||
|
submitButtonText?: string;
|
||||||
|
resetButtonText?: string;
|
||||||
|
gridProps?: GridProps;
|
||||||
|
giProps?: GridItemProps;
|
||||||
|
resetFunc?: () => Promise<void>;
|
||||||
|
submitFunc?: () => Promise<void>;
|
||||||
|
submitOnReset?: boolean;
|
||||||
|
baseGridStyle?: CSSProperties;
|
||||||
|
collapsedRows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormActionType {
|
||||||
|
submit: () => Promise<any>;
|
||||||
|
setProps: (formProps: Partial<FormProps>) => Promise<void>;
|
||||||
|
setSchema: (schemaProps: Partial<FormSchema[]>) => Promise<void>;
|
||||||
|
setFieldsValue: (values: Recordable) => void;
|
||||||
|
clearValidate: (name?: string | string[]) => Promise<void>;
|
||||||
|
getFieldsValue: () => Recordable;
|
||||||
|
resetFields: () => Promise<void>;
|
||||||
|
validate: (nameList?: any[]) => Promise<any>;
|
||||||
|
setLoading: (status: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegisterFn = (formInstance: FormActionType) => void;
|
||||||
|
|
||||||
|
export type UseFormReturnType = [RegisterFn, FormActionType];
|
||||||
28
src/components/Form/src/types/index.ts
Normal file
28
src/components/Form/src/types/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type ComponentType =
|
||||||
|
| 'NInput'
|
||||||
|
| 'NInputGroup'
|
||||||
|
| 'NInputPassword'
|
||||||
|
| 'NInputSearch'
|
||||||
|
| 'NInputTextArea'
|
||||||
|
| 'NInputNumber'
|
||||||
|
| 'NInputCountDown'
|
||||||
|
| 'NSelect'
|
||||||
|
| 'NTreeSelect'
|
||||||
|
| 'NRadioButtonGroup'
|
||||||
|
| 'NRadioGroup'
|
||||||
|
| 'NCheckbox'
|
||||||
|
| 'NCheckboxGroup'
|
||||||
|
| 'NAutoComplete'
|
||||||
|
| 'NCascader'
|
||||||
|
| 'NDatePicker'
|
||||||
|
| 'NMonthPicker'
|
||||||
|
| 'NRangePicker'
|
||||||
|
| 'NWeekPicker'
|
||||||
|
| 'NTimePicker'
|
||||||
|
| 'NSwitch'
|
||||||
|
| 'NStrengthMeter'
|
||||||
|
| 'NUpload'
|
||||||
|
| 'NIconPicker'
|
||||||
|
| 'NRender'
|
||||||
|
| 'NSlider'
|
||||||
|
| 'NRate';
|
||||||
@@ -1,29 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="{ onLockLogin: showLogin }"
|
:class="{ onLockLogin: showLogin }"
|
||||||
class="lockscreen"
|
class="lockscreen"
|
||||||
@keyup="onLockLogin(true)"
|
@keyup="onLockLogin(true)"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@contextmenu.prevent
|
@contextmenu.prevent
|
||||||
>
|
>
|
||||||
<template v-if="!showLogin">
|
<template v-if="!showLogin">
|
||||||
|
|
||||||
<div class="lock-box">
|
<div class="lock-box">
|
||||||
<div class="lock">
|
<div class="lock">
|
||||||
<span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)">
|
<span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)">
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<lock-outlined/>
|
<lock-outlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!--充电-->
|
<!--充电-->
|
||||||
<recharge
|
<recharge
|
||||||
:battery="battery"
|
:battery="battery"
|
||||||
:battery-status="batteryStatus"
|
:battery-status="batteryStatus"
|
||||||
:calc-discharging-time="calcDischargingTime"
|
:calc-discharging-time="calcDischargingTime"
|
||||||
></recharge>
|
:calc-charging-time="calcChargingTime"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="local-time">
|
<div class="local-time">
|
||||||
<div class="time">{{ hour }}:{{ minute }}</div>
|
<div class="time">{{ hour }}:{{ minute }}</div>
|
||||||
@@ -31,9 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="computer-status">
|
<div class="computer-status">
|
||||||
<span :class="{ offline: !online }" class="network">
|
<span :class="{ offline: !online }" class="network">
|
||||||
<wifi-outlined class="network"/>
|
<wifi-outlined class="network" />
|
||||||
</span>
|
</span>
|
||||||
<api-outlined/>
|
<api-outlined />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -42,263 +41,264 @@
|
|||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<n-avatar :size="128">
|
<n-avatar :size="128">
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<user-outlined/>
|
<user-outlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</n-avatar>
|
</n-avatar>
|
||||||
<div class="username">{{ loginParams.username }}</div>
|
<div class="username">{{ loginParams.username }}</div>
|
||||||
<n-input
|
<n-input
|
||||||
type="password"
|
type="password"
|
||||||
autofocus
|
autofocus
|
||||||
v-model:value="loginParams.password"
|
v-model:value="loginParams.password"
|
||||||
placeholder="请输入登录密码">
|
@keyup.enter="onLogin"
|
||||||
|
placeholder="请输入登录密码"
|
||||||
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-icon @click="onLogin" style="cursor: pointer;">
|
<n-icon @click="onLogin" style="cursor: pointer">
|
||||||
<LoadingOutlined v-if="loginLoading"/>
|
<LoadingOutlined v-if="loginLoading" />
|
||||||
<arrow-right-outlined v-else/>
|
<arrow-right-outlined v-else />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
|
|
||||||
<div class="w-full flex" v-if="isLoginError">
|
<div class="flex w-full" v-if="isLoginError">
|
||||||
<span class="text-red-500">{{ errorMsg }}</span>
|
<span class="text-red-500">{{ errorMsg }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-1 flex justify-around">
|
<div class="flex justify-around w-full mt-1">
|
||||||
<div><a @click="showLogin=false">返回</a></div>
|
<div><a @click="showLogin = false">返回</a></div>
|
||||||
<div><a @click="goLogin">重新登录</a></div>
|
<div><a @click="goLogin">重新登录</a></div>
|
||||||
<div><a @click="onLogin">进入系统</a></div>
|
<div><a @click="onLogin">进入系统</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, reactive, toRefs, computed } from 'vue'
|
import { defineComponent, reactive, toRefs } from 'vue';
|
||||||
import { ResultEnum } from '@/enums/httpEnum'
|
import { ResultEnum } from '@/enums/httpEnum';
|
||||||
import recharge from './Recharge.vue'
|
import recharge from './Recharge.vue';
|
||||||
import {
|
import {
|
||||||
LockOutlined,
|
|
||||||
LoadingOutlined,
|
|
||||||
UnlockOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
ApiOutlined,
|
|
||||||
ArrowRightOutlined,
|
|
||||||
WifiOutlined,
|
|
||||||
} from '@vicons/antd'
|
|
||||||
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { useOnline } from '@/hooks/useOnline'
|
|
||||||
import { useTime } from '@/hooks/useTime'
|
|
||||||
import { useBattery } from '@/hooks/useBattery'
|
|
||||||
import { useLockscreenStore } from '@/store/modules/lockscreen'
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Lockscreen',
|
|
||||||
components: {
|
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
UnlockOutlined,
|
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
ArrowRightOutlined,
|
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
|
ArrowRightOutlined,
|
||||||
WifiOutlined,
|
WifiOutlined,
|
||||||
recharge,
|
} from '@vicons/antd';
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const useLockscreen = useLockscreenStore()
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
// 获取时间
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
const { month, day, hour, minute, second, week } = useTime()
|
import { useOnline } from '@/hooks/useOnline';
|
||||||
const { online } = useOnline()
|
import { useTime } from '@/hooks/useTime';
|
||||||
|
import { useBattery } from '@/hooks/useBattery';
|
||||||
|
import { useScreenLockStore } from '@/store/modules/screenLock';
|
||||||
|
import { UserInfoType, useUserStore } from '@/store/modules/user';
|
||||||
|
|
||||||
const router = useRouter()
|
export default defineComponent({
|
||||||
const route = useRoute()
|
name: 'ScreenLock',
|
||||||
|
components: {
|
||||||
|
LockOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
ArrowRightOutlined,
|
||||||
|
ApiOutlined,
|
||||||
|
WifiOutlined,
|
||||||
|
recharge,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const useScreenLock = useScreenLockStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const { battery, batteryStatus, calcDischargingTime } = useBattery()
|
// 获取时间
|
||||||
const { username } = userStore.getUserInfo || {}
|
const { month, day, hour, minute, second, week } = useTime();
|
||||||
const state = reactive({
|
const { online } = useOnline();
|
||||||
showLogin: false,
|
|
||||||
loginLoading: false, // 正在登录
|
|
||||||
isLoginError: false, //密码错误
|
|
||||||
errorMsg: '密码错误',
|
|
||||||
loginParams: {
|
|
||||||
username: username || '',
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 解锁登录
|
const router = useRouter();
|
||||||
const onLockLogin = (value: boolean) => (state.showLogin = value)
|
const route = useRoute();
|
||||||
|
|
||||||
// 登录
|
const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
|
||||||
const onLogin = async () => {
|
const userInfo: UserInfoType = userStore.getUserInfo || {};
|
||||||
if (!state.loginParams.password.trim()) {
|
const username = userInfo['username'] || '';
|
||||||
return
|
const state = reactive({
|
||||||
}
|
showLogin: false,
|
||||||
const params = {
|
loginLoading: false, // 正在登录
|
||||||
isLock: true,
|
isLoginError: false, //密码错误
|
||||||
...state.loginParams
|
errorMsg: '密码错误',
|
||||||
}
|
loginParams: {
|
||||||
state.loginLoading = true
|
username: username || '',
|
||||||
const { code, result, message } = await userStore.login(params)
|
password: '',
|
||||||
if (code === ResultEnum.SUCCESS) {
|
},
|
||||||
onLockLogin(false)
|
});
|
||||||
useLockscreen.setLock(false)
|
|
||||||
} else {
|
|
||||||
state.errorMsg = message
|
|
||||||
state.isLoginError = true
|
|
||||||
}
|
|
||||||
state.loginLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
//重新登录
|
// 解锁登录
|
||||||
const goLogin = () => {
|
const onLockLogin = (value: boolean) => (state.showLogin = value);
|
||||||
onLockLogin(false)
|
|
||||||
useLockscreen.setLock(false)
|
// 登录
|
||||||
router.replace({
|
const onLogin = async () => {
|
||||||
path: '/login',
|
if (!state.loginParams.password.trim()) {
|
||||||
query: {
|
return;
|
||||||
redirect: route.fullPath
|
|
||||||
}
|
}
|
||||||
})
|
const params = {
|
||||||
}
|
isLock: true,
|
||||||
|
...state.loginParams,
|
||||||
|
};
|
||||||
|
state.loginLoading = true;
|
||||||
|
const { code, message } = await userStore.login(params);
|
||||||
|
if (code === ResultEnum.SUCCESS) {
|
||||||
|
onLockLogin(false);
|
||||||
|
useScreenLock.setLock(false);
|
||||||
|
} else {
|
||||||
|
state.errorMsg = message;
|
||||||
|
state.isLoginError = true;
|
||||||
|
}
|
||||||
|
state.loginLoading = false;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
//重新登录
|
||||||
...toRefs(state),
|
const goLogin = () => {
|
||||||
online,
|
onLockLogin(false);
|
||||||
month,
|
useScreenLock.setLock(false);
|
||||||
day,
|
router.replace({
|
||||||
hour,
|
path: '/login',
|
||||||
minute,
|
query: {
|
||||||
second,
|
redirect: route.fullPath,
|
||||||
week,
|
},
|
||||||
battery,
|
});
|
||||||
batteryStatus,
|
};
|
||||||
calcDischargingTime,
|
|
||||||
onLockLogin,
|
return {
|
||||||
onLogin,
|
...toRefs(state),
|
||||||
goLogin
|
online,
|
||||||
}
|
month,
|
||||||
}
|
day,
|
||||||
})
|
hour,
|
||||||
|
minute,
|
||||||
|
second,
|
||||||
|
week,
|
||||||
|
battery,
|
||||||
|
batteryStatus,
|
||||||
|
calcDischargingTime,
|
||||||
|
calcChargingTime,
|
||||||
|
onLockLogin,
|
||||||
|
onLogin,
|
||||||
|
goLogin,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.lockscreen {
|
.lockscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
display: flex;
|
|
||||||
background: #000;
|
|
||||||
color: white;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 9999;
|
|
||||||
|
|
||||||
&.onLockLogin {
|
|
||||||
background-color: rgba(25, 28, 34, 0.88);
|
|
||||||
backdrop-filter: blur(7px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-box {
|
|
||||||
position: absolute;
|
|
||||||
top: 45%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
background: #000;
|
||||||
justify-content: center;
|
color: white;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
> * {
|
&.onLockLogin {
|
||||||
margin-bottom: 14px;
|
background-color: rgba(25, 28, 34, 0.88);
|
||||||
|
backdrop-filter: blur(7px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.login-box {
|
||||||
font-size: 30px;
|
position: absolute;
|
||||||
}
|
top: 45%;
|
||||||
}
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
.lock-box {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 34px;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
color: white;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lock {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.lock-icon {
|
> * {
|
||||||
cursor: pointer;
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.anticon-unlock {
|
.username {
|
||||||
display: none;
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 34px;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
color: white;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.lock-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.anticon-unlock {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .anticon-unlock {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .anticon-lock {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover .anticon-unlock {
|
.local-time {
|
||||||
display: initial;
|
position: absolute;
|
||||||
}
|
bottom: 60px;
|
||||||
|
left: 60px;
|
||||||
|
font-family: helvetica;
|
||||||
|
|
||||||
&:hover .anticon-lock {
|
.time {
|
||||||
display: none;
|
font-size: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.computer-status {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
right: 60px;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.offline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 28px;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
background-color: red;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.local-time {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 60px;
|
|
||||||
left: 60px;
|
|
||||||
font-family: helvetica;
|
|
||||||
|
|
||||||
.time {
|
|
||||||
font-size: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.computer-status {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 60px;
|
|
||||||
right: 60px;
|
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin-left: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.offline::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 2px;
|
|
||||||
height: 28px;
|
|
||||||
transform: translate(-50%, -50%) rotate(45deg);
|
|
||||||
background-color: red;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,165 +13,152 @@
|
|||||||
剩余可使用时间:{{ calcDischargingTime }}
|
剩余可使用时间:{{ calcDischargingTime }}
|
||||||
</div>
|
</div>
|
||||||
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
|
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
|
||||||
距离电池充满需要:{{ calcDischargingTime }}
|
距离电池充满需要:{{ calcChargingTime }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'HuaweiCharge',
|
name: 'HuaweiCharge',
|
||||||
// props: ['batteryStatus', 'battery', 'calcDischargingTime'],
|
// props: ['batteryStatus', 'battery', 'calcDischargingTime'],
|
||||||
props: {
|
props: {
|
||||||
battery: {
|
battery: {
|
||||||
// 电池对象
|
// 电池对象
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
calcDischargingTime: {
|
||||||
|
// 电池剩余时间可用时间
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
calcChargingTime: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
batteryStatus: {
|
||||||
|
// 电池状态
|
||||||
|
type: String,
|
||||||
|
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
calcDischargingTime: {
|
});
|
||||||
// 电池剩余时间可用时间
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
batteryStatus: {
|
|
||||||
// 电池状态
|
|
||||||
type: String,
|
|
||||||
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.container {
|
.container {
|
||||||
position: absolute;
|
|
||||||
bottom: 20vh;
|
|
||||||
left: 50vw;
|
|
||||||
width: 300px;
|
|
||||||
height: 400px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
|
|
||||||
.number {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 27%;
|
bottom: 20vh;
|
||||||
z-index: 10;
|
left: 50vw;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
font-size: 32px;
|
height: 500px;
|
||||||
color: #fff;
|
transform: translateX(-50%);
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contrast {
|
.number {
|
||||||
width: 300px;
|
|
||||||
height: 400px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #000;
|
|
||||||
filter: contrast(15) hue-rotate(0);
|
|
||||||
animation: hueRotate 10s infinite linear;
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
position: relative;
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
filter: blur(8px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 40%;
|
|
||||||
left: 50%;
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
background-color: #00ff6f;
|
|
||||||
border-radius: 42% 38% 62% 49% / 45%;
|
|
||||||
content: '';
|
|
||||||
transform: translate(-50%, -50%) rotate(0);
|
|
||||||
animation: rotate 10s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 40%;
|
|
||||||
left: 50%;
|
|
||||||
z-index: 10;
|
|
||||||
width: 176px;
|
|
||||||
height: 176px;
|
|
||||||
background-color: #000;
|
|
||||||
border-radius: 50%;
|
|
||||||
content: '';
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubbles {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 20%;
|
||||||
left: 50%;
|
z-index: 10;
|
||||||
width: 100px;
|
width: 300px;
|
||||||
height: 40px;
|
font-size: 32px;
|
||||||
background-color: #00ff6f;
|
color: #fff;
|
||||||
border-radius: 100px 100px 0 0;
|
text-align: center;
|
||||||
filter: blur(5px);
|
}
|
||||||
transform: translate(-50%, 0);
|
|
||||||
|
|
||||||
li {
|
.contrast {
|
||||||
position: absolute;
|
width: 300px;
|
||||||
background: #00ff6f;
|
height: 400px;
|
||||||
border-radius: 50%;
|
overflow: hidden;
|
||||||
|
background-color: #000;
|
||||||
|
filter: contrast(15) hue-rotate(0);
|
||||||
|
animation: hueRotate 10s infinite linear;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
filter: blur(8px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #00ff6f;
|
||||||
|
border-radius: 42% 38% 62% 49% / 45%;
|
||||||
|
content: '';
|
||||||
|
transform: translate(-50%, -50%) rotate(0);
|
||||||
|
animation: rotate 10s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
width: 176px;
|
||||||
|
height: 176px;
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: '';
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubbles {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: #00ff6f;
|
||||||
|
border-radius: 100px 100px 0 0;
|
||||||
|
filter: blur(5px);
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: absolute;
|
||||||
|
background: #00ff6f;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charging {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.charging {
|
@keyframes rotate {
|
||||||
font-size: 20px;
|
50% {
|
||||||
text-align: center;
|
border-radius: 45% / 42% 38% 58% 49%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@width: ~`Math.round(Math.random() * 100)` px;
|
100% {
|
||||||
@left: calc(15px + `Math.round(Math.random(70))`);
|
transform: translate(-50%, -50%) rotate(720deg);
|
||||||
each(range(15), {
|
}
|
||||||
.xiaoma-@{value} {
|
|
||||||
height: (@value * 50px);
|
|
||||||
}
|
|
||||||
li:nth-child(@{index}) {
|
|
||||||
top: 50%;
|
|
||||||
left: @left;
|
|
||||||
width: @width;
|
|
||||||
height: @width;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
/*animation: moveToTop (Math.random(6) + 3s) ease-in-out -(Math.random(5000) / 1000s) infinite;*/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
50% {
|
|
||||||
border-radius: 45% / 42% 38% 58% 49%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
@keyframes moveToTop {
|
||||||
transform: translate(-50%, -50%) rotate(720deg);
|
90% {
|
||||||
}
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes moveToTop {
|
100% {
|
||||||
90% {
|
opacity: 0.1;
|
||||||
opacity: 1;
|
transform: translate(-50%, -180px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
@keyframes hueRotate {
|
||||||
opacity: 0.1;
|
100% {
|
||||||
transform: translate(-50%, -180px);
|
filter: contrast(15) hue-rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes hueRotate {
|
|
||||||
100% {
|
|
||||||
filter: contrast(15) hue-rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import LockScreen from './Lockscreen.vue'
|
import LockScreen from './Lockscreen.vue';
|
||||||
|
|
||||||
export { LockScreen }
|
export { LockScreen };
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import MessageContent from './index.vue'
|
|
||||||
|
|
||||||
export { MessageContent }
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<template></template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'MessageContent',
|
|
||||||
setup() {
|
|
||||||
//挂载在 window 方便与在js中使用
|
|
||||||
window.$message = useMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
3
src/components/Modal/index.ts
Normal file
3
src/components/Modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as basicModal } from './src/basicModal.vue';
|
||||||
|
export { useModal } from './src/hooks/useModal';
|
||||||
|
export * from './src/type';
|
||||||
109
src/components/Modal/src/basicModal.vue
Normal file
109
src/components/Modal/src/basicModal.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal id="basic-modal" v-bind="getBindValue" v-model:show="isModal" @close="onCloseModal">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-full cursor-move" id="basic-modal-bar">{{ getBindValue.title }}</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<slot name="default"></slot>
|
||||||
|
</template>
|
||||||
|
<template #action v-if="!$slots.action">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="closeModal">取消</n-button>
|
||||||
|
<n-button type="primary" :loading="subLoading" @click="handleSubmit">{{
|
||||||
|
subBtuText
|
||||||
|
}}</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<template v-else #action>
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getCurrentInstance, ref, nextTick, unref, computed, useAttrs } from 'vue';
|
||||||
|
import { basicProps } from './props';
|
||||||
|
import startDrag from '@/utils/Drag';
|
||||||
|
import { deepMerge } from '@/utils';
|
||||||
|
import { FormProps } from '@/components/Form';
|
||||||
|
import { ModalProps, ModalMethods } from './type';
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const props = defineProps({ ...basicProps });
|
||||||
|
const emit = defineEmits(['on-close', 'on-ok', 'register']);
|
||||||
|
|
||||||
|
const propsRef = ref<Partial<ModalProps> | null>(null);
|
||||||
|
|
||||||
|
const isModal = ref(false);
|
||||||
|
const subLoading = ref(false);
|
||||||
|
|
||||||
|
const getProps = computed((): FormProps => {
|
||||||
|
return { ...props, ...(unref(propsRef) as any) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const subBtuText = computed(() => {
|
||||||
|
const { subBtuText } = propsRef.value as any;
|
||||||
|
return subBtuText || props.subBtuText;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setProps(modalProps: Partial<ModalProps>): Promise<void> {
|
||||||
|
propsRef.value = deepMerge(unref(propsRef) || ({} as any), modalProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBindValue = computed(() => {
|
||||||
|
return {
|
||||||
|
...attrs,
|
||||||
|
...unref(getProps),
|
||||||
|
...unref(propsRef),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function setSubLoading(status: boolean) {
|
||||||
|
subLoading.value = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
isModal.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
const oBox = document.getElementById('basic-modal');
|
||||||
|
const oBar = document.getElementById('basic-modal-bar');
|
||||||
|
startDrag(oBar, oBox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
isModal.value = false;
|
||||||
|
subLoading.value = false;
|
||||||
|
emit('on-close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCloseModal() {
|
||||||
|
isModal.value = false;
|
||||||
|
emit('on-close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
subLoading.value = true;
|
||||||
|
emit('on-ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalMethods: ModalMethods = {
|
||||||
|
setProps,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
setSubLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
if (instance) {
|
||||||
|
emit('register', modalMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose(modalMethods);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.cursor-move {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
src/components/Modal/src/hooks/useModal.ts
Normal file
54
src/components/Modal/src/hooks/useModal.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ref, unref, getCurrentInstance, watch } from 'vue';
|
||||||
|
import { isProdMode } from '@/utils/env';
|
||||||
|
import { ModalMethods, UseModalReturnType } from '../type';
|
||||||
|
import { getDynamicProps } from '@/utils';
|
||||||
|
import { tryOnUnmounted } from '@vueuse/core';
|
||||||
|
export function useModal(props): UseModalReturnType {
|
||||||
|
const modalRef = ref<Nullable<ModalMethods>>(null);
|
||||||
|
const currentInstance = getCurrentInstance();
|
||||||
|
|
||||||
|
const getInstance = () => {
|
||||||
|
const instance = unref(modalRef.value);
|
||||||
|
if (!instance) {
|
||||||
|
console.error('useModal instance is undefined!');
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = (modalInstance: ModalMethods) => {
|
||||||
|
isProdMode() &&
|
||||||
|
tryOnUnmounted(() => {
|
||||||
|
modalRef.value = null;
|
||||||
|
});
|
||||||
|
modalRef.value = modalInstance;
|
||||||
|
currentInstance?.emit('register', modalInstance);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props,
|
||||||
|
() => {
|
||||||
|
props && modalInstance.setProps(getDynamicProps(props));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods: ModalMethods = {
|
||||||
|
setProps: (props): void => {
|
||||||
|
getInstance()?.setProps(props);
|
||||||
|
},
|
||||||
|
openModal: () => {
|
||||||
|
getInstance()?.openModal();
|
||||||
|
},
|
||||||
|
closeModal: () => {
|
||||||
|
getInstance()?.closeModal();
|
||||||
|
},
|
||||||
|
setSubLoading: (status) => {
|
||||||
|
getInstance()?.setSubLoading(status);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [register, methods];
|
||||||
|
}
|
||||||
30
src/components/Modal/src/props.ts
Normal file
30
src/components/Modal/src/props.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { NModal } from 'naive-ui';
|
||||||
|
|
||||||
|
export const basicProps = {
|
||||||
|
...NModal.props,
|
||||||
|
// 确认按钮文字
|
||||||
|
subBtuText: {
|
||||||
|
type: String,
|
||||||
|
default: '确认',
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 446,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
maskClosable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
preset: {
|
||||||
|
type: String,
|
||||||
|
default: 'dialog',
|
||||||
|
},
|
||||||
|
};
|
||||||
19
src/components/Modal/src/type/index.ts
Normal file
19
src/components/Modal/src/type/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { DialogOptions } from 'naive-ui/lib/dialog';
|
||||||
|
/**
|
||||||
|
* @description: 弹窗对外暴露的方法
|
||||||
|
*/
|
||||||
|
export interface ModalMethods {
|
||||||
|
setProps: (props) => void;
|
||||||
|
openModal: () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
setSubLoading: (status) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持修改,DialogOptions 參數
|
||||||
|
*/
|
||||||
|
export type ModalProps = DialogOptions;
|
||||||
|
|
||||||
|
export type RegisterFn = (ModalInstance: ModalMethods) => void;
|
||||||
|
|
||||||
|
export type UseModalReturnType = [RegisterFn, ModalMethods];
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
BasicTable 重封装组件说明
|
|
||||||
====
|
|
||||||
|
|
||||||
封装说明
|
|
||||||
----
|
|
||||||
|
|
||||||
> 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。
|
|
||||||
>
|
|
||||||
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 BasicTable 组件传递绑定 `:api="Promise"` 对象即可
|
|
||||||
>
|
|
||||||
> 例子1
|
|
||||||
----
|
|
||||||
(基础使用)
|
|
||||||
|
|
||||||
```vue
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BasicTable
|
|
||||||
title="表格列表"
|
|
||||||
:columns="columns"
|
|
||||||
:api="loadDataTable"
|
|
||||||
:row-key="row => row.id"
|
|
||||||
@update:checked-row-keys="onCheckedRow"
|
|
||||||
>
|
|
||||||
<template #toolbar>
|
|
||||||
<n-button type="primary">添加会员</n-button>
|
|
||||||
</template>
|
|
||||||
</BasicTable>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
import { BasicTable } from '@/components/Table'
|
|
||||||
import { getTableList } from '@/api/table/list'
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'id',
|
|
||||||
key: 'id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '名称',
|
|
||||||
key: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '地址',
|
|
||||||
key: 'address',
|
|
||||||
auth: ['amdin'], // 同时根据权限控制是否显示
|
|
||||||
ifShow: (row) => {
|
|
||||||
return true; // 根据业务控制是否显示
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '日期',
|
|
||||||
key: 'date'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
export default defineComponent({
|
|
||||||
components: { BasicTable },
|
|
||||||
setup() {
|
|
||||||
const loadDataTable = async (params) => {
|
|
||||||
const data = await getTableList(params);
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
loadDataTable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
API
|
|
||||||
----
|
|
||||||
BasicTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。
|
|
||||||
|
|
||||||
> request:Promise 参考上面例子写法
|
|
||||||
> ref:可绑定ref 调用组件内部方法(data-table本身的方法和参数)
|
|
||||||
|
|
||||||
Methods
|
|
||||||
----
|
|
||||||
> reload:actionRef.value.reload()
|
|
||||||
|
|
||||||
> 其余方法,请打印查看
|
|
||||||
|
|
||||||
Slots
|
|
||||||
----
|
|
||||||
> 名称:tableTitle | 表格顶部左侧区域
|
|
||||||
> 名称:toolbar | 表格顶部右侧区域
|
|
||||||
|
|
||||||
|
|
||||||
更新时间
|
|
||||||
----
|
|
||||||
|
|
||||||
该文档最后更新于: 2021-07-12 PM 10:13
|
|
||||||
@@ -2,4 +2,3 @@ export { default as BasicTable } from './src/Table.vue';
|
|||||||
export { default as TableAction } from './src/components/TableAction.vue';
|
export { default as TableAction } from './src/components/TableAction.vue';
|
||||||
export * from './src/types/table';
|
export * from './src/types/table';
|
||||||
export * from './src/types/tableAction';
|
export * from './src/types/tableAction';
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
|
|
||||||
<!--顶部左侧区域-->
|
<!--顶部左侧区域-->
|
||||||
<div class="flex items-center table-toolbar-left ">
|
<div class="flex items-center table-toolbar-left">
|
||||||
<template v-if="title">
|
<template v-if="props.title">
|
||||||
<div class="table-toolbar-left-title">
|
<div class="table-toolbar-left-title">
|
||||||
{{ title }}
|
{{ props.title }}
|
||||||
<n-tooltip trigger="hover" v-if="titleTooltip">
|
<n-tooltip trigger="hover" v-if="props.titleTooltip">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon size="18" class="ml-1 cursor-pointer text-gray-400">
|
<n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
|
||||||
<QuestionCircleOutlined/>
|
<QuestionCircleOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
{{ titleTooltip }}
|
{{ props.titleTooltip }}
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<slot name="tableTitle"></slot>
|
<slot name="tableTitle"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center table-toolbar-right">
|
<div class="flex items-center leading-none table-toolbar-right">
|
||||||
|
|
||||||
<!--顶部右侧区域-->
|
<!--顶部右侧区域-->
|
||||||
<slot name="toolbar"></slot>
|
<slot name="toolbar"></slot>
|
||||||
|
|
||||||
|
<!--斑马纹-->
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="mr-2 table-toolbar-right-icon">
|
||||||
|
<n-switch v-model:value="isStriped" @update:value="setStriped" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span>表格斑马纹</span>
|
||||||
|
</n-tooltip>
|
||||||
|
<n-divider vertical />
|
||||||
|
|
||||||
<!--刷新-->
|
<!--刷新-->
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="table-toolbar-right-icon" @click="reload">
|
<div class="table-toolbar-right-icon" @click="reload">
|
||||||
<n-icon size="18">
|
<n-icon size="18">
|
||||||
<ReloadOutlined/>
|
<ReloadOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,9 +49,14 @@
|
|||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="table-toolbar-right-icon">
|
<div class="table-toolbar-right-icon">
|
||||||
<n-dropdown @select="densitySelect" trigger="click" :options="densityOptions" v-model:value="tableSize">
|
<n-dropdown
|
||||||
|
@select="densitySelect"
|
||||||
|
trigger="click"
|
||||||
|
:options="densityOptions"
|
||||||
|
v-model:value="tableSize"
|
||||||
|
>
|
||||||
<n-icon size="18">
|
<n-icon size="18">
|
||||||
<ColumnHeightOutlined/>
|
<ColumnHeightOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,17 +65,17 @@
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<!--表格设置单独抽离成组件-->
|
<!--表格设置单独抽离成组件-->
|
||||||
<ColumnSetting></ColumnSetting>
|
<ColumnSetting />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="s-table">
|
<div class="s-table">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
v-bind="getBindValues"
|
ref="tableElRef"
|
||||||
:pagination="pagination"
|
v-bind="getBindValues"
|
||||||
@update:page="updatePage"
|
:striped="isStriped"
|
||||||
@update:page-size="updatePageSize"
|
:pagination="pagination"
|
||||||
|
@update:page="updatePage"
|
||||||
|
@update:page-size="updatePageSize"
|
||||||
>
|
>
|
||||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||||
<slot :name="item" v-bind="data"></slot>
|
<slot :name="item" v-bind="data"></slot>
|
||||||
@@ -70,227 +84,229 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { NDataTable } from 'naive-ui'
|
import { ref, unref, toRaw, computed, onMounted, nextTick } from 'vue';
|
||||||
import { ref, defineComponent, reactive, unref, onMounted, toRaw, onBeforeMount, computed, toRefs, watch } from "vue"
|
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
|
||||||
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, QuestionCircleOutlined } from '@vicons/antd'
|
import { createTableContext } from './hooks/useTableContext';
|
||||||
import { createTableContext } from './hooks/useTableContext';
|
|
||||||
|
|
||||||
import ColumnSetting from './components/settings/ColumnSetting.vue'
|
import ColumnSetting from './components/settings/ColumnSetting.vue';
|
||||||
|
|
||||||
import { useLoading } from './hooks/useLoading';
|
import { useLoading } from './hooks/useLoading';
|
||||||
import { useColumns } from './hooks/useColumns';
|
import { useColumns } from './hooks/useColumns';
|
||||||
import { useDataSource } from './hooks/useDataSource';
|
import { useDataSource } from './hooks/useDataSource';
|
||||||
import { usePagination } from './hooks/usePagination';
|
import { usePagination } from './hooks/usePagination';
|
||||||
|
|
||||||
import { basicProps } from './props'
|
import { basicProps } from './props';
|
||||||
|
|
||||||
import { BasicTableProps } from './types/table'
|
import { BasicTableProps } from './types/table';
|
||||||
|
|
||||||
|
import { getViewportOffset } from '@/utils/domUtils';
|
||||||
|
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
|
||||||
|
import { isBoolean } from '@/utils/is';
|
||||||
|
|
||||||
const densityOptions = [
|
const densityOptions = [
|
||||||
{
|
{
|
||||||
type: "menu",
|
type: 'menu',
|
||||||
label: '紧凑',
|
label: '紧凑',
|
||||||
key: 'small',
|
key: 'small',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "menu",
|
type: 'menu',
|
||||||
label: '默认',
|
label: '默认',
|
||||||
key: "medium"
|
key: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "menu",
|
type: 'menu',
|
||||||
label: '宽松',
|
label: '宽松',
|
||||||
key: 'large'
|
key: 'large',
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
const emit = defineEmits([
|
||||||
components: {
|
|
||||||
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, ColumnSetting, QuestionCircleOutlined
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
...NDataTable.props, // 这里继承原 UI 组件的 props
|
|
||||||
...basicProps
|
|
||||||
},
|
|
||||||
emits: [
|
|
||||||
'fetch-success',
|
'fetch-success',
|
||||||
'fetch-error',
|
'fetch-error',
|
||||||
'update:checked-row-keys'
|
'update:checked-row-keys',
|
||||||
],
|
'edit-end',
|
||||||
setup(props, { emit }) {
|
'edit-cancel',
|
||||||
|
'edit-row-end',
|
||||||
|
'edit-change',
|
||||||
|
]);
|
||||||
|
|
||||||
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
const props = defineProps({ ...basicProps });
|
||||||
|
const deviceHeight = ref(150);
|
||||||
|
const tableElRef = ref<ComponentRef>(null);
|
||||||
|
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
let paginationEl: HTMLElement | null;
|
||||||
|
const isStriped = ref(props.striped || false);
|
||||||
|
const tableData = ref<Recordable[]>([]);
|
||||||
|
const innerPropsRef = ref<Partial<BasicTableProps>>();
|
||||||
|
|
||||||
const tableData = ref<Recordable[]>([]);
|
const getProps = computed(() => {
|
||||||
const innerPropsRef = ref<Partial<BasicTableProps>>();
|
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
|
||||||
|
});
|
||||||
|
|
||||||
const getProps = computed(() => {
|
const tableSize = ref(unref(getProps as any).size || 'medium');
|
||||||
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getLoading, setLoading } = useLoading(getProps);
|
const { getLoading, setLoading } = useLoading(getProps);
|
||||||
|
|
||||||
const {
|
const { getPaginationInfo, setPagination } = usePagination(getProps);
|
||||||
|
|
||||||
|
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
|
||||||
|
getProps,
|
||||||
|
{
|
||||||
getPaginationInfo,
|
getPaginationInfo,
|
||||||
getPagination,
|
|
||||||
setPagination,
|
setPagination,
|
||||||
setShowPagination,
|
tableData,
|
||||||
getShowPagination,
|
|
||||||
} = usePagination(getProps)
|
|
||||||
|
|
||||||
const { getDataSourceRef, getRowKey, getDataSource, setDataSource, reload } = useDataSource(
|
|
||||||
getProps, {
|
|
||||||
getPaginationInfo,
|
|
||||||
setPagination,
|
|
||||||
tableData,
|
|
||||||
setLoading
|
|
||||||
}, emit
|
|
||||||
)
|
|
||||||
|
|
||||||
const {
|
|
||||||
getPageColumns,
|
|
||||||
setColumns,
|
|
||||||
getColumns,
|
|
||||||
getCacheColumns,
|
|
||||||
setCacheColumnsField,
|
|
||||||
getColumnsRef
|
|
||||||
} = useColumns(getProps)
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
tableSize: 'medium',
|
|
||||||
isColumnSetting: false
|
|
||||||
})
|
|
||||||
|
|
||||||
//页码切换
|
|
||||||
function updatePage(page) {
|
|
||||||
setPagination({ page: page, });
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
//分页数量切换
|
|
||||||
function updatePageSize(size) {
|
|
||||||
setPagination({ page: 1, pageSize: size, });
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
//密度切换
|
|
||||||
function densitySelect(e) {
|
|
||||||
state.tableSize = e
|
|
||||||
}
|
|
||||||
|
|
||||||
//选中行
|
|
||||||
function updateCheckedRowKeys(rowKeys) {
|
|
||||||
emit('update:checked-row-keys', rowKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
//重置 Columns
|
|
||||||
const resetColumns = () => {
|
|
||||||
columns.map(item => {
|
|
||||||
item.isShow = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取表格大小
|
|
||||||
const getTableSize = computed(() => state.tableSize)
|
|
||||||
|
|
||||||
//组装表格信息
|
|
||||||
const getBindValues = computed(() => {
|
|
||||||
const tableData = unref(getDataSourceRef);
|
|
||||||
let propsData = {
|
|
||||||
...unref(getProps),
|
|
||||||
loading: unref(getLoading),
|
|
||||||
columns: toRaw(unref(getPageColumns)),
|
|
||||||
rowKey: unref(getRowKey),
|
|
||||||
data: tableData,
|
|
||||||
size: unref(getTableSize),
|
|
||||||
remote: true
|
|
||||||
}
|
|
||||||
return propsData
|
|
||||||
})
|
|
||||||
|
|
||||||
//获取分页信息
|
|
||||||
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
|
|
||||||
|
|
||||||
function setProps(props: Partial<BasicTableProps>) {
|
|
||||||
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableAction: TableActionType = {
|
|
||||||
reload,
|
|
||||||
setColumns,
|
|
||||||
setLoading,
|
setLoading,
|
||||||
setProps,
|
},
|
||||||
getColumns,
|
emit
|
||||||
getPageColumns,
|
);
|
||||||
getCacheColumns,
|
|
||||||
setCacheColumnsField,
|
|
||||||
emit,
|
|
||||||
getSize: () => {
|
|
||||||
return unref(getBindValues).size as SizeType;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
createTableContext({ ...tableAction, wrapRef, getBindValues });
|
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
|
||||||
|
useColumns(getProps);
|
||||||
|
|
||||||
return {
|
//页码切换
|
||||||
...toRefs(state),
|
function updatePage(page) {
|
||||||
getBindValues,
|
setPagination({ page: page });
|
||||||
densityOptions,
|
reload();
|
||||||
reload,
|
|
||||||
densitySelect,
|
|
||||||
updatePage,
|
|
||||||
updatePageSize,
|
|
||||||
updateCheckedRowKeys,
|
|
||||||
pagination,
|
|
||||||
resetColumns,
|
|
||||||
tableAction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
|
//分页数量切换
|
||||||
|
function updatePageSize(size) {
|
||||||
|
setPagination({ page: 1, pageSize: size });
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
//密度切换
|
||||||
|
function densitySelect(e) {
|
||||||
|
tableSize.value = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取表格大小
|
||||||
|
const getTableSize = computed(() => tableSize.value);
|
||||||
|
|
||||||
|
//组装表格信息
|
||||||
|
const getBindValues = computed(() => {
|
||||||
|
const tableData = unref(getDataSourceRef);
|
||||||
|
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
|
||||||
|
return {
|
||||||
|
...unref(getProps),
|
||||||
|
loading: unref(getLoading),
|
||||||
|
columns: toRaw(unref(getPageColumns)),
|
||||||
|
rowKey: unref(getRowKey),
|
||||||
|
data: tableData,
|
||||||
|
size: unref(getTableSize),
|
||||||
|
remote: true,
|
||||||
|
'max-height': maxHeight,
|
||||||
|
title: '', // 重置为空 避免绑定到 table 上面
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//获取分页信息
|
||||||
|
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
|
||||||
|
|
||||||
|
function setProps(props: Partial<BasicTableProps>) {
|
||||||
|
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStriped = (value: boolean) => (isStriped.value = value);
|
||||||
|
|
||||||
|
const tableAction = {
|
||||||
|
reload,
|
||||||
|
setColumns,
|
||||||
|
setLoading,
|
||||||
|
setProps,
|
||||||
|
getColumns,
|
||||||
|
getDataSource,
|
||||||
|
getPageColumns,
|
||||||
|
getCacheColumns,
|
||||||
|
setCacheColumnsField,
|
||||||
|
emit,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCanResize = computed(() => {
|
||||||
|
const { canResize } = unref(getProps);
|
||||||
|
return canResize;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function computeTableHeight() {
|
||||||
|
const table = unref(tableElRef);
|
||||||
|
if (!table) return;
|
||||||
|
if (!unref(getCanResize)) return;
|
||||||
|
const tableEl: any = table?.$el;
|
||||||
|
const headEl = tableEl.querySelector('.n-data-table-thead ');
|
||||||
|
const { bottomIncludeBody } = getViewportOffset(headEl);
|
||||||
|
const headerH = 64;
|
||||||
|
let paginationH = 2;
|
||||||
|
let marginH = 24;
|
||||||
|
if (!isBoolean(unref(pagination))) {
|
||||||
|
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
|
||||||
|
if (paginationEl) {
|
||||||
|
const offsetHeight = paginationEl.offsetHeight;
|
||||||
|
paginationH += offsetHeight || 0;
|
||||||
|
} else {
|
||||||
|
paginationH += 28;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let height =
|
||||||
|
bottomIncludeBody - (headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
|
||||||
|
const maxHeight = props.maxHeight;
|
||||||
|
height = maxHeight && maxHeight < height ? maxHeight : height;
|
||||||
|
deviceHeight.value = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
useWindowSizeFn(computeTableHeight, 280);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
computeTableHeight();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
createTableContext({ ...tableAction, wrapRef, getBindValues });
|
||||||
|
|
||||||
|
defineExpose(tableAction);
|
||||||
</script>
|
</script>
|
||||||
<style lang='less' scoped>
|
<style lang="less" scoped>
|
||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 0 16px 0;
|
|
||||||
|
|
||||||
&-left {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
justify-content: flex-start;
|
padding: 0 0 16px 0;
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&-title {
|
&-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
font-weight: 600;
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&-right {
|
&-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.table-toolbar-inner-popover-title {
|
.table-toolbar-inner-popover-title {
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
41
src/components/Table/src/componentMap.ts
Normal file
41
src/components/Table/src/componentMap.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
import {
|
||||||
|
NInput,
|
||||||
|
NSelect,
|
||||||
|
NCheckbox,
|
||||||
|
NInputNumber,
|
||||||
|
NSwitch,
|
||||||
|
NDatePicker,
|
||||||
|
NTimePicker,
|
||||||
|
} from 'naive-ui';
|
||||||
|
import type { ComponentType } from './types/componentType';
|
||||||
|
|
||||||
|
export enum EventEnum {
|
||||||
|
NInput = 'on-input',
|
||||||
|
NInputNumber = 'on-input',
|
||||||
|
NSelect = 'on-update:value',
|
||||||
|
NSwitch = 'on-update:value',
|
||||||
|
NCheckbox = 'on-update:value',
|
||||||
|
NDatePicker = 'on-update:value',
|
||||||
|
NTimePicker = 'on-update:value',
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentMap = new Map<ComponentType, Component>();
|
||||||
|
|
||||||
|
componentMap.set('NInput', NInput);
|
||||||
|
componentMap.set('NInputNumber', NInputNumber);
|
||||||
|
componentMap.set('NSelect', NSelect);
|
||||||
|
componentMap.set('NSwitch', NSwitch);
|
||||||
|
componentMap.set('NCheckbox', NCheckbox);
|
||||||
|
componentMap.set('NDatePicker', NDatePicker);
|
||||||
|
componentMap.set('NTimePicker', NTimePicker);
|
||||||
|
|
||||||
|
export function add(compName: ComponentType, component: Component) {
|
||||||
|
componentMap.set(compName, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function del(compName: ComponentType) {
|
||||||
|
componentMap.delete(compName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { componentMap };
|
||||||
@@ -2,21 +2,25 @@
|
|||||||
<div class="tableAction">
|
<div class="tableAction">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
|
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
|
||||||
<n-button v-bind="action" class="mx-2">{{ action.label }}</n-button>
|
<n-button v-bind="action" class="mx-1">
|
||||||
|
{{ action.label }}
|
||||||
|
<template #icon v-if="action.hasOwnProperty('icon')">
|
||||||
|
<n-icon :component="action.icon" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<n-dropdown
|
<n-dropdown
|
||||||
v-if="dropDownActions && getDropdownList.length"
|
v-if="dropDownActions && getDropdownList.length"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
:options="getDropdownList"
|
:options="getDropdownList"
|
||||||
@select="select"
|
@select="select"
|
||||||
>
|
>
|
||||||
<slot name="more"></slot>
|
<slot name="more"></slot>
|
||||||
<n-button v-bind="getMoreProps" class="mx-2" v-if="!$slots.more" icon-placement="right">
|
<n-button v-bind="getMoreProps" class="mx-1" v-if="!$slots.more" icon-placement="right">
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span>更多</span>
|
<span>更多</span>
|
||||||
<n-icon size="14" class="ml-1">
|
<n-icon size="14" class="ml-1">
|
||||||
<DownOutlined/>
|
<DownOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
<!-- <template #icon>-->
|
<!-- <template #icon>-->
|
||||||
@@ -29,64 +33,57 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, computed, toRaw } from 'vue';
|
import { defineComponent, PropType, computed, toRaw } from 'vue';
|
||||||
import { ActionItem } from '@/components/Table';
|
import { ActionItem } from '@/components/Table';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { isString, isBoolean, isFunction } from "@/utils/is";
|
import { isBoolean, isFunction } from '@/utils/is';
|
||||||
import { DownOutlined } from '@vicons/antd'
|
import { DownOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TableAction',
|
name: 'TableAction',
|
||||||
components: { DownOutlined },
|
components: { DownOutlined },
|
||||||
props: {
|
props: {
|
||||||
actions: {
|
actions: {
|
||||||
type: Array as PropType<ActionItem[]>,
|
type: Array as PropType<ActionItem[]>,
|
||||||
default: null,
|
default: null,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dropDownActions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: String as PropType<String>,
|
||||||
|
default: 'button',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
type: Function as PropType<Function>,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dropDownActions: {
|
setup(props) {
|
||||||
type: Array as PropType<ActionItem[]>,
|
const { hasPermission } = usePermission();
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
type: String as PropType<String>,
|
|
||||||
default: 'button'
|
|
||||||
},
|
|
||||||
select:{
|
|
||||||
type: Function as PropType<Function>,
|
|
||||||
default: () =>{ }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const { hasPermission } = usePermission();
|
|
||||||
|
|
||||||
const getTooltip = computed(() => {
|
const actionType =
|
||||||
return (data: string | TooltipProps): TooltipProps => {
|
props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default';
|
||||||
if (isString(data)) {
|
const actionText =
|
||||||
return { title: data, placement: 'bottom' };
|
props.style === 'button' ? undefined : props.style === 'text' ? true : undefined;
|
||||||
} else {
|
|
||||||
return Object.assign({ placement: 'bottom' }, data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionType = props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default'
|
const getMoreProps = computed(() => {
|
||||||
const actionText = props.style === 'button' ? undefined : props.style === 'text' ? true : undefined
|
|
||||||
|
|
||||||
const getMoreProps = computed(() => {
|
|
||||||
return {
|
return {
|
||||||
text: actionText,
|
text: actionText,
|
||||||
type: actionType,
|
type: actionType,
|
||||||
size: "small"
|
size: 'small',
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
const getDropdownList = computed(() => {
|
const getDropdownList = computed(() => {
|
||||||
return (toRaw(props.dropDownActions) || [])
|
return (toRaw(props.dropDownActions) || [])
|
||||||
.filter((action) => {
|
.filter((action) => {
|
||||||
return hasPermission(action.auth) && isIfShow(action);
|
return hasPermission(action.auth as string[]) && isIfShow(action);
|
||||||
})
|
})
|
||||||
.map((action, index) => {
|
.map((action) => {
|
||||||
const { label, popConfirm } = action;
|
const { popConfirm } = action;
|
||||||
return {
|
return {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
text: actionText,
|
text: actionText,
|
||||||
@@ -94,29 +91,29 @@ export default defineComponent({
|
|||||||
...action,
|
...action,
|
||||||
...popConfirm,
|
...popConfirm,
|
||||||
onConfirm: popConfirm?.confirm,
|
onConfirm: popConfirm?.confirm,
|
||||||
onCancel: popConfirm?.cancel
|
onCancel: popConfirm?.cancel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function isIfShow(action: ActionItem): boolean {
|
function isIfShow(action: ActionItem): boolean {
|
||||||
const ifShow = action.ifShow;
|
const ifShow = action.ifShow;
|
||||||
|
|
||||||
let isIfShow = true;
|
let isIfShow = true;
|
||||||
|
|
||||||
if (isBoolean(ifShow)) {
|
if (isBoolean(ifShow)) {
|
||||||
isIfShow = ifShow;
|
isIfShow = ifShow;
|
||||||
|
}
|
||||||
|
if (isFunction(ifShow)) {
|
||||||
|
isIfShow = ifShow(action);
|
||||||
|
}
|
||||||
|
return isIfShow;
|
||||||
}
|
}
|
||||||
if (isFunction(ifShow)) {
|
|
||||||
isIfShow = ifShow(action);
|
|
||||||
}
|
|
||||||
return isIfShow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActions = computed(() => {
|
const getActions = computed(() => {
|
||||||
return (toRaw(props.actions) || [])
|
return (toRaw(props.actions) || [])
|
||||||
.filter((action) => {
|
.filter((action) => {
|
||||||
return hasPermission(action.auth) && isIfShow(action);
|
return hasPermission(action.auth as string[]) && isIfShow(action);
|
||||||
})
|
})
|
||||||
.map((action) => {
|
.map((action) => {
|
||||||
const { popConfirm } = action;
|
const { popConfirm } = action;
|
||||||
@@ -132,14 +129,13 @@ export default defineComponent({
|
|||||||
enable: !!popConfirm,
|
enable: !!popConfirm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getActions,
|
getActions,
|
||||||
getDropdownList,
|
getDropdownList,
|
||||||
getTooltip,
|
getMoreProps,
|
||||||
getMoreProps
|
};
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { FunctionalComponent, defineComponent } from 'vue';
|
||||||
|
import type { ComponentType } from '../../types/componentType';
|
||||||
|
import { componentMap } from '@/components/Table/src/componentMap';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { NPopover } from 'naive-ui';
|
||||||
|
|
||||||
|
export interface ComponentProps {
|
||||||
|
component: ComponentType;
|
||||||
|
rule: boolean;
|
||||||
|
popoverVisible: boolean;
|
||||||
|
ruleMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CellComponent: FunctionalComponent = (
|
||||||
|
{ component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps,
|
||||||
|
{ attrs }
|
||||||
|
) => {
|
||||||
|
const Comp = componentMap.get(component) as typeof defineComponent;
|
||||||
|
|
||||||
|
const DefaultComp = h(Comp, attrs);
|
||||||
|
if (!rule) {
|
||||||
|
return DefaultComp;
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
NPopover,
|
||||||
|
{ 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' },
|
||||||
|
{
|
||||||
|
trigger: () => DefaultComp,
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
color: 'red',
|
||||||
|
width: '90px',
|
||||||
|
display: 'inline-block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => ruleMessage,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
418
src/components/Table/src/components/editable/EditableCell.vue
Normal file
418
src/components/Table/src/components/editable/EditableCell.vue
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editable-cell">
|
||||||
|
<div class="flex editable-cell-content" v-if="isEdit" v-click-outside="onClickOutside">
|
||||||
|
<div class="editable-cell-content-comp">
|
||||||
|
<CellComponent
|
||||||
|
v-bind="getComponentProps"
|
||||||
|
:component="getComponent"
|
||||||
|
:popoverVisible="getRuleVisible"
|
||||||
|
:ruleMessage="ruleMessage"
|
||||||
|
:rule="getRule"
|
||||||
|
:class="getWrapperClass"
|
||||||
|
ref="elRef"
|
||||||
|
@options-change="handleOptionsChange"
|
||||||
|
@press-enter="handleEnter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="editable-cell-action" v-if="!getRowEditable">
|
||||||
|
<n-icon class="mx-2 cursor-pointer" title="保存">
|
||||||
|
<CheckOutlined @click="handleSubmit" />
|
||||||
|
</n-icon>
|
||||||
|
<n-icon class="mx-2 cursor-pointer" title="取消">
|
||||||
|
<CloseOutlined @click="handleCancel" />
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center editable-cell-content" @click="handleEdit">
|
||||||
|
{{ getValues }}
|
||||||
|
<n-icon class="ml-1 edit-icon" v-if="!column.editRow">
|
||||||
|
<FormOutlined />
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import type { BasicColumn } from '../../types/table';
|
||||||
|
import type { EditRecordRow } from './index';
|
||||||
|
|
||||||
|
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
|
||||||
|
import { FormOutlined, CloseOutlined, CheckOutlined } from '@vicons/antd';
|
||||||
|
import { CellComponent } from './CellComponent';
|
||||||
|
|
||||||
|
import { useTableContext } from '../../hooks/useTableContext';
|
||||||
|
|
||||||
|
import clickOutside from '@/directives/clickOutside';
|
||||||
|
|
||||||
|
import { propTypes } from '@/utils/propTypes';
|
||||||
|
import { isString, isBoolean, isFunction, isNumber, isArray } from '@/utils/is';
|
||||||
|
import { createPlaceholderMessage } from './helper';
|
||||||
|
import { set, omit } from 'lodash-es';
|
||||||
|
import { EventEnum } from '@/components/Table/src/componentMap';
|
||||||
|
|
||||||
|
import { parseISO, format } from 'date-fns';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'EditableCell',
|
||||||
|
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
|
||||||
|
directives: {
|
||||||
|
clickOutside,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
type: Object as PropType<EditRecordRow>,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
type: Object as PropType<BasicColumn>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
index: propTypes.number,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const table = useTableContext();
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const elRef = ref();
|
||||||
|
const ruleVisible = ref(false);
|
||||||
|
const ruleMessage = ref('');
|
||||||
|
const optionsRef = ref<LabelValueOptions>([]);
|
||||||
|
const currentValueRef = ref<any>(props.value);
|
||||||
|
const defaultValueRef = ref<any>(props.value);
|
||||||
|
|
||||||
|
// const { prefixCls } = useDesign('editable-cell');
|
||||||
|
|
||||||
|
const getComponent = computed(() => props.column?.editComponent || 'NInput');
|
||||||
|
const getRule = computed(() => props.column?.editRule);
|
||||||
|
|
||||||
|
const getRuleVisible = computed(() => {
|
||||||
|
return unref(ruleMessage) && unref(ruleVisible);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getIsCheckComp = computed(() => {
|
||||||
|
const component = unref(getComponent);
|
||||||
|
return ['NCheckbox', 'NRadio'].includes(component);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getComponentProps = computed(() => {
|
||||||
|
const compProps = props.column?.editComponentProps ?? {};
|
||||||
|
const editComponent = props.column?.editComponent ?? null;
|
||||||
|
const component = unref(getComponent);
|
||||||
|
const apiSelectProps: Recordable = {};
|
||||||
|
|
||||||
|
const isCheckValue = unref(getIsCheckComp);
|
||||||
|
|
||||||
|
let valueField = isCheckValue ? 'checked' : 'value';
|
||||||
|
const val = unref(currentValueRef);
|
||||||
|
|
||||||
|
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
|
||||||
|
|
||||||
|
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||||
|
if (component === 'NDatePicker') {
|
||||||
|
if (isString(value)) {
|
||||||
|
if (compProps.valueFormat) {
|
||||||
|
valueField = 'formatted-value';
|
||||||
|
} else {
|
||||||
|
value = parseISO(value as any).getTime();
|
||||||
|
}
|
||||||
|
} else if (isArray(value)) {
|
||||||
|
if (compProps.valueFormat) {
|
||||||
|
valueField = 'formatted-value';
|
||||||
|
} else {
|
||||||
|
value = value.map((item) => parseISO(item).getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
placeholder: createPlaceholderMessage(unref(getComponent)),
|
||||||
|
...apiSelectProps,
|
||||||
|
...omit(compProps, 'onChange'),
|
||||||
|
[onEvent]: handleChange,
|
||||||
|
[valueField]: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getValues = computed(() => {
|
||||||
|
const { editComponentProps, editValueMap } = props.column;
|
||||||
|
|
||||||
|
const value = unref(currentValueRef);
|
||||||
|
|
||||||
|
if (editValueMap && isFunction(editValueMap)) {
|
||||||
|
return editValueMap(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = unref(getComponent);
|
||||||
|
if (!component.includes('NSelect')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
|
||||||
|
const option = options.find((item) => `${item.value}` === `${value}`);
|
||||||
|
|
||||||
|
return option?.label ?? value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWrapperClass = computed(() => {
|
||||||
|
const { align = 'center' } = props.column;
|
||||||
|
return `edit-cell-align-${align}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRowEditable = computed(() => {
|
||||||
|
const { editable } = props.record || {};
|
||||||
|
return !!editable;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
defaultValueRef.value = props.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const { editable } = props.column;
|
||||||
|
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
|
||||||
|
isEdit.value = !!editable || unref(getRowEditable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
|
||||||
|
ruleMessage.value = '';
|
||||||
|
isEdit.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
const el = unref(elRef);
|
||||||
|
el?.focus?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleChange(e: any) {
|
||||||
|
const component = unref(getComponent);
|
||||||
|
const compProps = props.column?.editComponentProps ?? {};
|
||||||
|
if (!e) {
|
||||||
|
currentValueRef.value = e;
|
||||||
|
} else if (e?.target && Reflect.has(e.target, 'value')) {
|
||||||
|
currentValueRef.value = (e as ChangeEvent).target.value;
|
||||||
|
} else if (component === 'NCheckbox') {
|
||||||
|
currentValueRef.value = (e as ChangeEvent).target.checked;
|
||||||
|
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
|
||||||
|
currentValueRef.value = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||||
|
if (component === 'NDatePicker') {
|
||||||
|
if (isNumber(currentValueRef.value)) {
|
||||||
|
if (compProps.valueFormat) {
|
||||||
|
currentValueRef.value = format(currentValueRef.value, compProps.valueFormat);
|
||||||
|
}
|
||||||
|
} else if (isArray(currentValueRef.value)) {
|
||||||
|
if (compProps.valueFormat) {
|
||||||
|
currentValueRef.value = currentValueRef.value.map((item) => {
|
||||||
|
format(item, compProps.valueFormat);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = props.column?.editComponentProps?.onChange;
|
||||||
|
if (onChange && isFunction(onChange)) onChange(...arguments);
|
||||||
|
|
||||||
|
table.emit?.('edit-change', {
|
||||||
|
column: props.column,
|
||||||
|
value: unref(currentValueRef),
|
||||||
|
record: toRaw(props.record),
|
||||||
|
});
|
||||||
|
await handleSubmiRule();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmiRule() {
|
||||||
|
const { column, record } = props;
|
||||||
|
const { editRule } = column;
|
||||||
|
const currentValue = unref(currentValueRef);
|
||||||
|
|
||||||
|
if (editRule) {
|
||||||
|
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
|
||||||
|
ruleVisible.value = true;
|
||||||
|
const component = unref(getComponent);
|
||||||
|
ruleMessage.value = createPlaceholderMessage(component);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isFunction(editRule)) {
|
||||||
|
const res = await editRule(currentValue, record as Recordable);
|
||||||
|
if (!!res) {
|
||||||
|
ruleMessage.value = res;
|
||||||
|
ruleVisible.value = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
ruleMessage.value = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleMessage.value = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(needEmit = true, valid = true) {
|
||||||
|
if (valid) {
|
||||||
|
const isPass = await handleSubmiRule();
|
||||||
|
if (!isPass) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { column, index, record } = props;
|
||||||
|
if (!record) return false;
|
||||||
|
const { key } = column;
|
||||||
|
const value = unref(currentValueRef);
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const dataKey = key as string;
|
||||||
|
|
||||||
|
set(record, dataKey, value);
|
||||||
|
//const record = await table.updateTableData(index, dataKey, value);
|
||||||
|
needEmit && table.emit?.('edit-end', { record, index, key, value });
|
||||||
|
isEdit.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEnter() {
|
||||||
|
if (props.column?.editRow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
isEdit.value = false;
|
||||||
|
currentValueRef.value = defaultValueRef.value;
|
||||||
|
const { column, index, record } = props;
|
||||||
|
const { key } = column;
|
||||||
|
ruleVisible.value = true;
|
||||||
|
ruleMessage.value = '';
|
||||||
|
table.emit?.('edit-cancel', {
|
||||||
|
record,
|
||||||
|
index,
|
||||||
|
key: key,
|
||||||
|
value: unref(currentValueRef),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickOutside() {
|
||||||
|
if (props.column?.editable || unref(getRowEditable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const component = unref(getComponent);
|
||||||
|
|
||||||
|
if (component.includes('NInput')) {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only ApiSelect
|
||||||
|
function handleOptionsChange(options: LabelValueOptions) {
|
||||||
|
optionsRef.value = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
|
||||||
|
if (props.record) {
|
||||||
|
/* eslint-disable */
|
||||||
|
isArray(props.record[cbs])
|
||||||
|
? props.record[cbs]?.push(handle)
|
||||||
|
: (props.record[cbs] = [handle]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.record) {
|
||||||
|
initCbs('submitCbs', handleSubmit);
|
||||||
|
initCbs('validCbs', handleSubmiRule);
|
||||||
|
initCbs('cancelCbs', handleCancel);
|
||||||
|
|
||||||
|
if (props.column.key) {
|
||||||
|
if (!props.record.editValueRefs) props.record.editValueRefs = {};
|
||||||
|
props.record.editValueRefs[props.column.key] = currentValueRef;
|
||||||
|
}
|
||||||
|
/* eslint-disable */
|
||||||
|
props.record.onCancelEdit = () => {
|
||||||
|
isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
|
||||||
|
};
|
||||||
|
/* eslint-disable */
|
||||||
|
props.record.onSubmitEdit = async () => {
|
||||||
|
if (isArray(props.record?.submitCbs)) {
|
||||||
|
const validFns = (props.record?.validCbs || []).map((fn) => fn());
|
||||||
|
|
||||||
|
const res = await Promise.all(validFns);
|
||||||
|
|
||||||
|
const pass = res.every((item) => !!item);
|
||||||
|
|
||||||
|
if (!pass) return;
|
||||||
|
const submitFns = props.record?.submitCbs || [];
|
||||||
|
submitFns.forEach((fn) => fn(false, false));
|
||||||
|
table.emit?.('edit-row-end');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEdit,
|
||||||
|
handleEdit,
|
||||||
|
currentValueRef,
|
||||||
|
handleSubmit,
|
||||||
|
handleChange,
|
||||||
|
handleCancel,
|
||||||
|
elRef,
|
||||||
|
getComponent,
|
||||||
|
getRule,
|
||||||
|
onClickOutside,
|
||||||
|
ruleMessage,
|
||||||
|
getRuleVisible,
|
||||||
|
getComponentProps,
|
||||||
|
handleOptionsChange,
|
||||||
|
getWrapperClass,
|
||||||
|
getRowEditable,
|
||||||
|
getValues,
|
||||||
|
handleEnter,
|
||||||
|
// getSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.editable-cell {
|
||||||
|
&-content {
|
||||||
|
position: relative;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&-comp {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
//position: absolute;
|
||||||
|
//top: 4px;
|
||||||
|
//right: 0;
|
||||||
|
display: none;
|
||||||
|
width: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.edit-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/components/Table/src/components/editable/helper.ts
Normal file
15
src/components/Table/src/components/editable/helper.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ComponentType } from '../../types/componentType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 生成placeholder
|
||||||
|
*/
|
||||||
|
export function createPlaceholderMessage(component: ComponentType) {
|
||||||
|
if (component === 'NInput') return '请输入';
|
||||||
|
if (
|
||||||
|
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
|
||||||
|
component
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return '请选择';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
49
src/components/Table/src/components/editable/index.ts
Normal file
49
src/components/Table/src/components/editable/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { BasicColumn } from '@/components/Table/src/types/table';
|
||||||
|
import { h, Ref } from 'vue';
|
||||||
|
|
||||||
|
import EditableCell from './EditableCell.vue';
|
||||||
|
|
||||||
|
export function renderEditCell(column: BasicColumn) {
|
||||||
|
return (record, index) => {
|
||||||
|
const _key = column.key;
|
||||||
|
const value = record[_key];
|
||||||
|
record.onEdit = async (edit: boolean, submit = false) => {
|
||||||
|
if (!submit) {
|
||||||
|
record.editable = edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!edit && submit) {
|
||||||
|
const res = await record.onSubmitEdit?.();
|
||||||
|
if (res) {
|
||||||
|
record.editable = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// cancel
|
||||||
|
if (!edit && !submit) {
|
||||||
|
record.onCancelEdit?.();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return h(EditableCell, {
|
||||||
|
value,
|
||||||
|
record,
|
||||||
|
column,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditRecordRow<T = Recordable> = Partial<
|
||||||
|
{
|
||||||
|
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
|
||||||
|
editable: boolean;
|
||||||
|
onCancel: Fn;
|
||||||
|
onSubmit: Fn;
|
||||||
|
submitCbs: Fn[];
|
||||||
|
cancelCbs: Fn[];
|
||||||
|
validCbs: Fn[];
|
||||||
|
editValueRefs: Recordable<Ref>;
|
||||||
|
} & T
|
||||||
|
>;
|
||||||
@@ -5,46 +5,75 @@
|
|||||||
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
|
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon size="18">
|
<n-icon size="18">
|
||||||
<SettingOutlined/>
|
<SettingOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="table-toolbar-inner-popover-title">
|
<div class="table-toolbar-inner-popover-title">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll">列展示</n-checkbox>
|
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll"
|
||||||
<n-checkbox v-model:checked="selection" @update:checked="onSelection">勾选列</n-checkbox>
|
>列展示</n-checkbox
|
||||||
<n-button text type="info" size="small" class="mt-1" @click="resetColumns">重置</n-button>
|
>
|
||||||
|
<n-checkbox v-model:checked="selection" @update:checked="onSelection"
|
||||||
|
>勾选列</n-checkbox
|
||||||
|
>
|
||||||
|
<n-button text type="info" size="small" class="mt-1" @click="resetColumns"
|
||||||
|
>重置</n-button
|
||||||
|
>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="table-toolbar-inner">
|
<div class="table-toolbar-inner">
|
||||||
<n-checkbox-group v-model:value="checkList" @update:value="onChange">
|
<n-checkbox-group v-model:value="checkList" @update:value="onChange">
|
||||||
<Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd">
|
<Draggable
|
||||||
<template #item="{element, index}">
|
v-model="columnsList"
|
||||||
<div class="table-toolbar-inner-checkbox"
|
animation="300"
|
||||||
:class="{'table-toolbar-inner-checkbox-dark':getDarkTheme === true}">
|
item-key="key"
|
||||||
<span class="drag-icon">
|
filter=".no-draggable"
|
||||||
|
:move="onMove"
|
||||||
|
@end="draggableEnd"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<div
|
||||||
|
class="table-toolbar-inner-checkbox"
|
||||||
|
:class="{
|
||||||
|
'table-toolbar-inner-checkbox-dark': getDarkTheme === true,
|
||||||
|
'no-draggable': element.draggable === false,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="drag-icon"
|
||||||
|
:class="{ 'drag-icon-hidden': element.draggable === false }"
|
||||||
|
>
|
||||||
<n-icon size="18">
|
<n-icon size="18">
|
||||||
<DragOutlined/>
|
<DragOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</span>
|
</span>
|
||||||
<n-checkbox :value="element.key" :label="element.title"/>
|
<n-checkbox :value="element.key" :label="element.title" />
|
||||||
<div class="fixed-item">
|
<div class="fixed-item">
|
||||||
<n-tooltip trigger="hover" placement="bottom">
|
<n-tooltip trigger="hover" placement="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon size="18" :color="element.fixed === 'left' ? '#2080f0':undefined"
|
<n-icon
|
||||||
class="cursor-pointer" @click="fixedColumn(element,'left')">
|
size="18"
|
||||||
<VerticalRightOutlined/>
|
:color="element.fixed === 'left' ? '#2080f0' : undefined"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="fixedColumn(element, 'left')"
|
||||||
|
>
|
||||||
|
<VerticalRightOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
<span>固定到左侧</span>
|
<span>固定到左侧</span>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
<n-divider vertical/>
|
<n-divider vertical />
|
||||||
<n-tooltip trigger="hover" placement="bottom">
|
<n-tooltip trigger="hover" placement="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-icon size="18" :color="element.fixed === 'right' ? '#2080f0':undefined"
|
<n-icon
|
||||||
class="cursor-pointer" @click="fixedColumn(element,'right')">
|
size="18"
|
||||||
<VerticalLeftOutlined/>
|
:color="element.fixed === 'right' ? '#2080f0' : undefined"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="fixedColumn(element, 'right')"
|
||||||
|
>
|
||||||
|
<VerticalLeftOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
<span>固定到右侧</span>
|
<span>固定到右侧</span>
|
||||||
@@ -63,221 +92,240 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from "vue"
|
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
|
||||||
import { useTableContext } from '../../hooks/useTableContext';
|
import { useTableContext } from '../../hooks/useTableContext';
|
||||||
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, VerticalRightOutlined, VerticalLeftOutlined } from '@vicons/antd'
|
import { cloneDeep } from 'lodash-es';
|
||||||
import Draggable from 'vuedraggable/src/vuedraggable'
|
import {
|
||||||
import { useDesignSetting } from "@/hooks/setting/useDesignSetting";
|
SettingOutlined,
|
||||||
|
DragOutlined,
|
||||||
|
VerticalRightOutlined,
|
||||||
|
VerticalLeftOutlined,
|
||||||
|
} from '@vicons/antd';
|
||||||
|
import Draggable from 'vuedraggable';
|
||||||
|
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
fixed?: boolean | 'left' | 'right';
|
fixed?: boolean | 'left' | 'right';
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'ColumnSetting',
|
|
||||||
components: {
|
|
||||||
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, Draggable,
|
|
||||||
VerticalRightOutlined, VerticalLeftOutlined
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const { getDarkTheme } = useDesignSetting()
|
|
||||||
const table = useTableContext();
|
|
||||||
const columnsList = ref<Options[]>([]);
|
|
||||||
const cacheColumnsList = ref<Options[]>([]);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
selection: false,
|
|
||||||
checkAll: true,
|
|
||||||
checkList: [],
|
|
||||||
defaultCheckList: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const getSelection = computed(() => {
|
|
||||||
return state.selection
|
|
||||||
})
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const columns = table.getColumns();
|
|
||||||
if (columns.length) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//初始化
|
|
||||||
function init() {
|
|
||||||
const columns = getColumns();
|
|
||||||
const checkList = columns.map(item => item.key)
|
|
||||||
state.checkList = checkList
|
|
||||||
state.defaultCheckList = checkList
|
|
||||||
columnsList.value = columns
|
|
||||||
cacheColumnsList.value = columns
|
|
||||||
}
|
|
||||||
|
|
||||||
//切换
|
|
||||||
function onChange(checkList) {
|
|
||||||
if (state.selection) {
|
|
||||||
checkList.unshift('selection')
|
|
||||||
}
|
|
||||||
setColumns(checkList)
|
|
||||||
}
|
|
||||||
|
|
||||||
//设置
|
|
||||||
function setColumns(columns) {
|
|
||||||
table.setColumns(columns)
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取
|
|
||||||
function getColumns() {
|
|
||||||
let newRet = []
|
|
||||||
table.getColumns().forEach(item => {
|
|
||||||
newRet.push({ ...item })
|
|
||||||
})
|
|
||||||
return newRet
|
|
||||||
}
|
|
||||||
|
|
||||||
//重置
|
|
||||||
function resetColumns() {
|
|
||||||
state.checkList = [...state.defaultCheckList]
|
|
||||||
state.checkAll = true;
|
|
||||||
let cacheColumnsKeys: any[] = table.getCacheColumns()
|
|
||||||
let newColumns = cacheColumnsKeys.map(item => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
fixed: undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setColumns(newColumns);
|
|
||||||
columnsList.value = newColumns
|
|
||||||
}
|
|
||||||
|
|
||||||
//全选
|
|
||||||
function onCheckAll(e) {
|
|
||||||
let checkList = table.getCacheColumns(true)
|
|
||||||
if (e) {
|
|
||||||
setColumns(checkList);
|
|
||||||
state.checkList = checkList
|
|
||||||
} else {
|
|
||||||
setColumns([]);
|
|
||||||
state.checkList = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//拖拽排序
|
|
||||||
function draggableEnd() {
|
|
||||||
const newColumns = toRaw(unref(columnsList))
|
|
||||||
columnsList.value = newColumns
|
|
||||||
setColumns(newColumns);
|
|
||||||
}
|
|
||||||
|
|
||||||
//勾选列
|
|
||||||
function onSelection(e) {
|
|
||||||
let checkList = table.getCacheColumns()
|
|
||||||
if (e) {
|
|
||||||
checkList.unshift({ type: 'selection', key: 'selection' })
|
|
||||||
setColumns(checkList);
|
|
||||||
} else {
|
|
||||||
checkList.splice(0, 1)
|
|
||||||
setColumns(checkList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//固定
|
|
||||||
function fixedColumn(item, fixed) {
|
|
||||||
console.log('item:', item)
|
|
||||||
if (!state.checkList.includes(item.key)) return;
|
|
||||||
let columns = getColumns();
|
|
||||||
const isFixed = item.fixed === fixed ? undefined : fixed
|
|
||||||
let index = columns.findIndex(res => res.key === item.key)
|
|
||||||
console.log('index:', index)
|
|
||||||
if (index !== -1) {
|
|
||||||
columns[index].fixed = isFixed;
|
|
||||||
}
|
|
||||||
table.setCacheColumnsField(item.key, { fixed: isFixed })
|
|
||||||
columnsList.value[index].fixed = isFixed
|
|
||||||
console.log('columnsList:', columnsList.value)
|
|
||||||
setColumns(columns);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
columnsList,
|
|
||||||
getDarkTheme,
|
|
||||||
onChange,
|
|
||||||
onCheckAll,
|
|
||||||
onSelection,
|
|
||||||
resetColumns,
|
|
||||||
fixedColumn,
|
|
||||||
draggableEnd,
|
|
||||||
getSelection
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ColumnSetting',
|
||||||
|
components: {
|
||||||
|
SettingOutlined,
|
||||||
|
DragOutlined,
|
||||||
|
Draggable,
|
||||||
|
VerticalRightOutlined,
|
||||||
|
VerticalLeftOutlined,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { getDarkTheme } = useDesignSetting();
|
||||||
|
const table: any = useTableContext();
|
||||||
|
const columnsList = ref<Options[]>([]);
|
||||||
|
const cacheColumnsList = ref<Options[]>([]);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
selection: false,
|
||||||
|
checkAll: true,
|
||||||
|
checkList: [],
|
||||||
|
defaultCheckList: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSelection = computed(() => {
|
||||||
|
return state.selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const columns = table.getColumns();
|
||||||
|
if (columns.length) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//初始化
|
||||||
|
function init() {
|
||||||
|
const columns: any[] = getColumns();
|
||||||
|
const checkList: any = columns.map((item) => item.key);
|
||||||
|
state.checkList = checkList;
|
||||||
|
state.defaultCheckList = checkList;
|
||||||
|
const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
|
||||||
|
if (!columnsList.value.length) {
|
||||||
|
columnsList.value = cloneDeep(newColumns);
|
||||||
|
cacheColumnsList.value = cloneDeep(newColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//切换
|
||||||
|
function onChange(checkList) {
|
||||||
|
if (state.selection) {
|
||||||
|
checkList.unshift('selection');
|
||||||
|
}
|
||||||
|
setColumns(checkList);
|
||||||
|
}
|
||||||
|
|
||||||
|
//设置
|
||||||
|
function setColumns(columns) {
|
||||||
|
table.setColumns(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取
|
||||||
|
function getColumns() {
|
||||||
|
let newRet: any[] = [];
|
||||||
|
table.getColumns().forEach((item) => {
|
||||||
|
newRet.push({ ...item });
|
||||||
|
});
|
||||||
|
return newRet;
|
||||||
|
}
|
||||||
|
|
||||||
|
//重置
|
||||||
|
function resetColumns() {
|
||||||
|
state.checkList = [...state.defaultCheckList];
|
||||||
|
state.checkAll = true;
|
||||||
|
let cacheColumnsKeys: any[] = table.getCacheColumns();
|
||||||
|
let newColumns = cacheColumnsKeys.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
fixed: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setColumns(newColumns);
|
||||||
|
columnsList.value = newColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
//全选
|
||||||
|
function onCheckAll(e) {
|
||||||
|
let checkList = table.getCacheColumns(true);
|
||||||
|
if (e) {
|
||||||
|
setColumns(checkList);
|
||||||
|
state.checkList = checkList;
|
||||||
|
} else {
|
||||||
|
setColumns([]);
|
||||||
|
state.checkList = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//拖拽排序
|
||||||
|
function draggableEnd() {
|
||||||
|
const newColumns = toRaw(unref(columnsList));
|
||||||
|
columnsList.value = newColumns;
|
||||||
|
setColumns(newColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
//勾选列
|
||||||
|
function onSelection(e) {
|
||||||
|
let checkList = table.getCacheColumns();
|
||||||
|
if (e) {
|
||||||
|
checkList.unshift({ type: 'selection', key: 'selection' });
|
||||||
|
setColumns(checkList);
|
||||||
|
} else {
|
||||||
|
checkList.splice(0, 1);
|
||||||
|
setColumns(checkList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMove(e) {
|
||||||
|
if (e.draggedContext.element.draggable === false) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//固定
|
||||||
|
function fixedColumn(item, fixed) {
|
||||||
|
if (!state.checkList.includes(item.key)) return;
|
||||||
|
let columns = getColumns();
|
||||||
|
const isFixed = item.fixed === fixed ? undefined : fixed;
|
||||||
|
let index = columns.findIndex((res) => res.key === item.key);
|
||||||
|
if (index !== -1) {
|
||||||
|
columns[index].fixed = isFixed;
|
||||||
|
}
|
||||||
|
table.setCacheColumnsField(item.key, { fixed: isFixed });
|
||||||
|
columnsList.value[index].fixed = isFixed;
|
||||||
|
setColumns(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
columnsList,
|
||||||
|
getDarkTheme,
|
||||||
|
onChange,
|
||||||
|
onCheckAll,
|
||||||
|
onSelection,
|
||||||
|
onMove,
|
||||||
|
resetColumns,
|
||||||
|
fixedColumn,
|
||||||
|
draggableEnd,
|
||||||
|
getSelection,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.table-toolbar {
|
.table-toolbar {
|
||||||
&-inner-popover-title {
|
&-inner-popover-title {
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-right {
|
&-right {
|
||||||
&-icon {
|
&-icon {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.table-toolbar-inner {
|
.table-toolbar-inner {
|
||||||
&-checkbox {
|
&-checkbox {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 14px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e6f7ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-right: 8px;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
padding: 10px 14px;
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-checkbox-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1890ff !important;
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: move;
|
||||||
|
&-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-checkbox-dark {
|
||||||
|
&:hover {
|
||||||
|
background: hsla(0, 0%, 100%, 0.08);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-checkbox-dark {
|
.toolbar-popover {
|
||||||
&:hover {
|
.n-popover__content {
|
||||||
background: hsla(0, 0%, 100%, .08);
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-popover {
|
|
||||||
.n-popover__content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import componentSetting from '@/settings/componentSetting'
|
import componentSetting from '@/settings/componentSetting';
|
||||||
|
|
||||||
const { table } = componentSetting
|
const { table } = componentSetting;
|
||||||
|
|
||||||
const { apiSetting, defaultPageSize, pageSizes } = table;
|
const { apiSetting, defaultPageSize, pageSizes } = table;
|
||||||
|
|
||||||
@@ -9,7 +9,3 @@ export const DEFAULTPAGESIZE = defaultPageSize;
|
|||||||
export const APISETTING = apiSetting;
|
export const APISETTING = apiSetting;
|
||||||
|
|
||||||
export const PAGESIZES = pageSizes;
|
export const PAGESIZES = pageSizes;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,126 +1,164 @@
|
|||||||
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue';
|
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue';
|
||||||
import type { BasicColumn, BasicTableProps } from '../types/table';
|
import type { BasicColumn, BasicTableProps } from '../types/table';
|
||||||
import { isEqual, cloneDeep } from 'lodash-es';
|
import { isEqual, cloneDeep } from 'lodash-es';
|
||||||
import { isArray, isString } from '@/utils/is';
|
import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { isString, isBoolean, isFunction } from "@/utils/is";
|
import { ActionItem } from '@/components/Table';
|
||||||
import { ActionItem } from "@/components/Table";
|
import { renderEditCell } from '../components/editable';
|
||||||
|
import { NTooltip, NIcon } from 'naive-ui';
|
||||||
|
import { FormOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
||||||
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
|
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
|
||||||
let cacheColumns = unref(propsRef).columns;
|
let cacheColumns = unref(propsRef).columns;
|
||||||
|
|
||||||
const getColumnsRef = computed(() => {
|
const getColumnsRef = computed(() => {
|
||||||
const columns = cloneDeep(unref(columnsRef));
|
const columns = cloneDeep(unref(columnsRef));
|
||||||
|
|
||||||
handleActionColumn(propsRef, columns);
|
handleActionColumn(propsRef, columns);
|
||||||
if (!columns) return [];
|
if (!columns) return [];
|
||||||
return columns;
|
return columns;
|
||||||
})
|
});
|
||||||
|
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
function isIfShow(action: ActionItem): boolean {
|
function isIfShow(action: ActionItem): boolean {
|
||||||
const ifShow = action.ifShow;
|
const ifShow = action.ifShow;
|
||||||
|
|
||||||
let isIfShow = true;
|
let isIfShow = true;
|
||||||
|
|
||||||
if (isBoolean(ifShow)) {
|
if (isBoolean(ifShow)) {
|
||||||
isIfShow = ifShow;
|
isIfShow = ifShow;
|
||||||
}
|
|
||||||
if (isFunction(ifShow)) {
|
|
||||||
isIfShow = ifShow(action);
|
|
||||||
}
|
|
||||||
return isIfShow;
|
|
||||||
}
|
}
|
||||||
|
if (isFunction(ifShow)) {
|
||||||
|
isIfShow = ifShow(action);
|
||||||
|
}
|
||||||
|
return isIfShow;
|
||||||
|
}
|
||||||
|
|
||||||
const getPageColumns = computed(() => {
|
const renderTooltip = (trigger, content) => {
|
||||||
const pageColumns = unref(getColumnsRef);
|
return h(NTooltip, null, {
|
||||||
const columns = cloneDeep(pageColumns);
|
trigger: () => trigger,
|
||||||
return columns.filter((column) => {
|
default: () => content,
|
||||||
return hasPermission(column.auth) && isIfShow(column);
|
});
|
||||||
})
|
};
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
const getPageColumns = computed(() => {
|
||||||
() => unref(propsRef).columns,
|
const pageColumns = unref(getColumnsRef);
|
||||||
(columns) => {
|
const columns = cloneDeep(pageColumns);
|
||||||
columnsRef.value = columns;
|
return columns
|
||||||
cacheColumns = columns;
|
.filter((column) => {
|
||||||
|
return hasPermission(column.auth as string[]) && isIfShow(column);
|
||||||
|
})
|
||||||
|
.map((column) => {
|
||||||
|
//默认 ellipsis 为true
|
||||||
|
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
|
||||||
|
const { edit } = column;
|
||||||
|
if (edit) {
|
||||||
|
column.render = renderEditCell(column);
|
||||||
|
if (edit) {
|
||||||
|
const title: any = column.title;
|
||||||
|
column.title = () => {
|
||||||
|
return renderTooltip(
|
||||||
|
h('div', { class: 'flex items-center' }, [
|
||||||
|
h('span', { style: { 'margin-right': '5px' } }, title),
|
||||||
|
h(
|
||||||
|
NIcon,
|
||||||
|
{
|
||||||
|
size: 14,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => h(FormOutlined),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
'该列可编辑'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
return column;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
|
watch(
|
||||||
const { actionColumn } = unref(propsRef);
|
() => unref(propsRef).columns,
|
||||||
if (!actionColumn) return;
|
(columns) => {
|
||||||
columns.push({
|
columnsRef.value = columns;
|
||||||
...actionColumn
|
cacheColumns = columns;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
|
||||||
|
const { actionColumn } = unref(propsRef);
|
||||||
|
if (!actionColumn) return;
|
||||||
|
!columns.find((col) => col.key === 'action') &&
|
||||||
|
columns.push({
|
||||||
|
...(actionColumn as any),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//设置
|
||||||
|
function setColumns(columnList: string[]) {
|
||||||
|
const columns: any[] = cloneDeep(columnList);
|
||||||
|
if (!isArray(columns)) return;
|
||||||
|
|
||||||
|
if (!columns.length) {
|
||||||
|
columnsRef.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cacheKeys = cacheColumns.map((item) => item.key);
|
||||||
|
//针对拖拽排序
|
||||||
|
if (!isString(columns[0])) {
|
||||||
|
columnsRef.value = columns;
|
||||||
|
} else {
|
||||||
|
const newColumns: any[] = [];
|
||||||
|
cacheColumns.forEach((item) => {
|
||||||
|
if (columnList.includes(item.key)) {
|
||||||
|
newColumns.push({ ...item });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isEqual(cacheKeys, columns)) {
|
||||||
|
newColumns.sort((prev, next) => {
|
||||||
|
return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
columnsRef.value = newColumns;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//设置
|
//获取
|
||||||
function setColumns(columnList: string[]) {
|
function getColumns(): BasicColumn[] {
|
||||||
const columns: any[] = cloneDeep(columnList);
|
const columns = toRaw(unref(getColumnsRef));
|
||||||
if (!isArray(columns)) return;
|
return columns.map((item) => {
|
||||||
|
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!columns.length) {
|
//获取原始
|
||||||
columnsRef.value = [];
|
function getCacheColumns(isKey?: boolean): any[] {
|
||||||
return;
|
return isKey ? cacheColumns.map((item) => item.key) : cacheColumns;
|
||||||
}
|
}
|
||||||
const cacheKeys = cacheColumns.map((item) => item.key);
|
|
||||||
//针对拖拽排序
|
//更新原始数据单个字段
|
||||||
if (!isString(columns[0])) {
|
function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
|
||||||
columnsRef.value = columns;
|
if (!key || !value) {
|
||||||
} else {
|
return;
|
||||||
const newColumns: any[] = []
|
|
||||||
cacheColumns.forEach(item => {
|
|
||||||
if (columnList.includes(item.key)) {
|
|
||||||
newColumns.push({ ...item })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!isEqual(cacheKeys, columns)) {
|
|
||||||
newColumns.sort((prev, next) => {
|
|
||||||
return (
|
|
||||||
cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
columnsRef.value = newColumns
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
cacheColumns.forEach((item) => {
|
||||||
|
if (item.key === key) {
|
||||||
|
Object.assign(item, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//获取
|
return {
|
||||||
function getColumns() {
|
getColumnsRef,
|
||||||
let columns = toRaw(unref(getColumnsRef));
|
getCacheColumns,
|
||||||
return columns.map(item => {
|
setCacheColumnsField,
|
||||||
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }
|
setColumns,
|
||||||
})
|
getColumns,
|
||||||
}
|
getPageColumns,
|
||||||
|
};
|
||||||
//获取原始
|
|
||||||
function getCacheColumns(isKey?: boolean): any[] {
|
|
||||||
return isKey ? cacheColumns.map(item => item.key) : cacheColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
//更新原始数据单个字段
|
|
||||||
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
|
|
||||||
if (!dataIndex || !value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cacheColumns.forEach((item) => {
|
|
||||||
if (item.key === dataIndex) {
|
|
||||||
Object.assign(item, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getColumnsRef,
|
|
||||||
getCacheColumns,
|
|
||||||
setCacheColumnsField,
|
|
||||||
setColumns,
|
|
||||||
getColumns,
|
|
||||||
getPageColumns
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,150 @@
|
|||||||
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
|
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
|
||||||
import type { BasicTableProps } from '../types/table';
|
import type { BasicTableProps } from '../types/table';
|
||||||
import type { PaginationProps } from '../types/pagination';
|
import type { PaginationProps } from '../types/pagination';
|
||||||
import { isBoolean } from '@/utils/is';
|
import { isBoolean, isFunction, isArray } from '@/utils/is';
|
||||||
import { APISETTING } from '../const';
|
import { APISETTING } from '../const';
|
||||||
|
|
||||||
export function useDataSource(
|
export function useDataSource(
|
||||||
propsRef: ComputedRef<BasicTableProps>,
|
propsRef: ComputedRef<BasicTableProps>,
|
||||||
{
|
{ getPaginationInfo, setPagination, setLoading, tableData },
|
||||||
getPaginationInfo,
|
emit
|
||||||
setPagination,
|
|
||||||
setLoading,
|
|
||||||
tableData
|
|
||||||
},
|
|
||||||
emit
|
|
||||||
) {
|
) {
|
||||||
const dataSourceRef = ref([]);
|
const dataSourceRef = ref<Recordable[]>([]);
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
tableData.value = unref(dataSourceRef);
|
tableData.value = unref(dataSourceRef);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => unref(propsRef).dataSource,
|
() => unref(propsRef).dataSource,
|
||||||
() => {
|
() => {
|
||||||
const { dataSource }: any = unref(propsRef);
|
const { dataSource }: any = unref(propsRef);
|
||||||
dataSource && (dataSourceRef.value = dataSource);
|
dataSource && (dataSourceRef.value = dataSource);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRowKey = computed(() => {
|
const getRowKey = computed(() => {
|
||||||
const { rowKey }: any = unref(propsRef);
|
const { rowKey }: any = unref(propsRef);
|
||||||
return rowKey ? rowKey : () => {
|
return rowKey
|
||||||
return 'key'
|
? rowKey
|
||||||
|
: () => {
|
||||||
|
return 'key';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDataSourceRef = computed(() => {
|
const getDataSourceRef = computed(() => {
|
||||||
const dataSource = unref(dataSourceRef);
|
const dataSource = unref(dataSourceRef);
|
||||||
if (!dataSource || dataSource.length === 0) {
|
if (!dataSource || dataSource.length === 0) {
|
||||||
return unref(dataSourceRef);
|
return unref(dataSourceRef);
|
||||||
}
|
}
|
||||||
return unref(dataSourceRef);
|
return unref(dataSourceRef);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetch(opt?) {
|
async function fetch(opt?) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { request, pagination }: any = unref(propsRef);
|
const { request, pagination, beforeRequest, afterRequest }: any = unref(propsRef);
|
||||||
|
if (!request) return;
|
||||||
//组装分页信息
|
//组装分页信息
|
||||||
const pageField = APISETTING.pageField
|
const pageField = APISETTING.pageField;
|
||||||
const sizeField = APISETTING.sizeField
|
const sizeField = APISETTING.sizeField;
|
||||||
const totalField = APISETTING.totalField
|
const totalField = APISETTING.totalField;
|
||||||
const listField = APISETTING.listField
|
const listField = APISETTING.listField;
|
||||||
|
const itemCount = APISETTING.countField;
|
||||||
let pageParams = {};
|
let pageParams = {};
|
||||||
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
|
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
|
||||||
|
|
||||||
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
|
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
|
||||||
pageParams = {};
|
pageParams = {};
|
||||||
} else {
|
} else {
|
||||||
pageParams[pageField] = (opt && opt[pageField]) || page;
|
pageParams[pageField] = (opt && opt[pageField]) || page;
|
||||||
pageParams[sizeField] = pageSize;
|
pageParams[sizeField] = pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = {
|
let params = {
|
||||||
...pageParams,
|
...pageParams,
|
||||||
}
|
...opt,
|
||||||
const res = await request(params);
|
};
|
||||||
|
if (beforeRequest && isFunction(beforeRequest)) {
|
||||||
const resultTotal = res[totalField] || 0
|
// The params parameter can be modified by outsiders
|
||||||
const currentPage = res[pageField]
|
params = (await beforeRequest(params)) || params;
|
||||||
|
}
|
||||||
// 如果数据异常,需获取正确的页码再次执行
|
const res = await request(params);
|
||||||
if (resultTotal) {
|
const resultTotal = res[totalField];
|
||||||
const currentTotalPage = Math.ceil(resultTotal / pageSize);
|
const currentPage = res[pageField];
|
||||||
if (page > currentTotalPage) {
|
const total = res[itemCount];
|
||||||
setPagination({
|
const results = res[listField] ? res[listField] : [];
|
||||||
[pageField]: currentTotalPage,
|
|
||||||
});
|
// 如果数据异常,需获取正确的页码再次执行
|
||||||
fetch(opt);
|
if (resultTotal) {
|
||||||
}
|
const currentTotalPage = Math.ceil(total / pageSize);
|
||||||
}
|
if (page > currentTotalPage) {
|
||||||
let resultInfo = res[listField] ? res[listField] : []
|
setPagination({
|
||||||
dataSourceRef.value = resultInfo;
|
page: currentTotalPage,
|
||||||
setPagination({
|
itemCount: total,
|
||||||
[pageField]: currentPage,
|
});
|
||||||
[totalField]: resultTotal,
|
return await fetch(opt);
|
||||||
});
|
|
||||||
if (opt && opt[pageField]) {
|
|
||||||
setPagination({
|
|
||||||
[pageField]: opt[pageField] || 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
emit('fetch-success', {
|
|
||||||
items: unref(resultInfo),
|
|
||||||
resultTotal
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
emit('fetch-error', error);
|
|
||||||
dataSourceRef.value = [];
|
|
||||||
// setPagination({
|
|
||||||
// pageCount: 0,
|
|
||||||
// });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let resultInfo = res[listField] ? res[listField] : [];
|
||||||
|
if (afterRequest && isFunction(afterRequest)) {
|
||||||
|
// can modify the data returned by the interface for processing
|
||||||
|
resultInfo = (await afterRequest(resultInfo)) || resultInfo;
|
||||||
|
}
|
||||||
|
dataSourceRef.value = resultInfo;
|
||||||
|
setPagination({
|
||||||
|
page: currentPage,
|
||||||
|
pageCount: resultTotal,
|
||||||
|
itemCount: total,
|
||||||
|
});
|
||||||
|
if (opt && opt[pageField]) {
|
||||||
|
setPagination({
|
||||||
|
page: opt[pageField] || 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emit('fetch-success', {
|
||||||
|
items: unref(resultInfo),
|
||||||
|
resultTotal,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
emit('fetch-error', error);
|
||||||
|
dataSourceRef.value = [];
|
||||||
|
setPagination({
|
||||||
|
pageCount: 0,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetch();
|
fetch();
|
||||||
}, 16)
|
}, 16);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setTableData(values) {
|
function setTableData(values) {
|
||||||
dataSourceRef.value = values;
|
dataSourceRef.value = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDataSource(): any[] {
|
function getDataSource(): any[] {
|
||||||
return getDataSourceRef.value;
|
return getDataSourceRef.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reload(opt?) {
|
async function reload(opt?) {
|
||||||
await fetch(opt);
|
await fetch(opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetch,
|
fetch,
|
||||||
getRowKey,
|
getRowKey,
|
||||||
getDataSourceRef,
|
getDataSourceRef,
|
||||||
getDataSource,
|
getDataSource,
|
||||||
setTableData,
|
setTableData,
|
||||||
reload
|
reload,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ import { ref, ComputedRef, unref, computed, watch } from 'vue';
|
|||||||
import type { BasicTableProps } from '../types/table';
|
import type { BasicTableProps } from '../types/table';
|
||||||
|
|
||||||
export function useLoading(props: ComputedRef<BasicTableProps>) {
|
export function useLoading(props: ComputedRef<BasicTableProps>) {
|
||||||
const loadingRef = ref(unref(props).loading);
|
const loadingRef = ref(unref(props).loading);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => unref(props).loading,
|
() => unref(props).loading,
|
||||||
(loading) => {
|
(loading) => {
|
||||||
loadingRef.value = loading;
|
loadingRef.value = loading;
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLoading = computed(() => unref(loadingRef));
|
|
||||||
|
|
||||||
function setLoading(loading: boolean) {
|
|
||||||
loadingRef.value = loading;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return { getLoading, setLoading };
|
const getLoading = computed(() => unref(loadingRef));
|
||||||
|
|
||||||
|
function setLoading(loading: boolean) {
|
||||||
|
loadingRef.value = loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getLoading, setLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,62 @@
|
|||||||
import type { PaginationProps } from '../types/pagination';
|
import type { PaginationProps } from '../types/pagination';
|
||||||
import type { BasicTableProps } from '../types/table';
|
import type { BasicTableProps } from '../types/table';
|
||||||
import { computed, unref, ref, ComputedRef } from 'vue';
|
import { computed, unref, ref, ComputedRef, watch } from 'vue';
|
||||||
|
|
||||||
import { isBoolean } from '@/utils/is';
|
import { isBoolean } from '@/utils/is';
|
||||||
import { DEFAULTPAGESIZE, PAGESIZES } from '../const';
|
import { DEFAULTPAGESIZE, PAGESIZES } from '../const';
|
||||||
|
|
||||||
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
|
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
|
||||||
const configRef = ref<PaginationProps>({});
|
const configRef = ref<PaginationProps>({});
|
||||||
const show = ref(true);
|
const show = ref(true);
|
||||||
|
|
||||||
const getPaginationInfo = computed((): PaginationProps | boolean => {
|
watch(
|
||||||
const { pagination } = unref(refProps);
|
() => unref(refProps).pagination,
|
||||||
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
|
(pagination) => {
|
||||||
return false;
|
if (!isBoolean(pagination) && pagination) {
|
||||||
}
|
|
||||||
return {
|
|
||||||
pageSize: DEFAULTPAGESIZE,
|
|
||||||
pageSizes: PAGESIZES,
|
|
||||||
showSizePicker: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
...(isBoolean(pagination) ? {} : pagination),
|
|
||||||
...unref(configRef),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function setPagination(info: Partial<PaginationProps>) {
|
|
||||||
const paginationInfo = unref(getPaginationInfo);
|
|
||||||
configRef.value = {
|
configRef.value = {
|
||||||
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
|
...unref(configRef),
|
||||||
...info,
|
...(pagination ?? {}),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function getPagination() {
|
const getPaginationInfo = computed((): PaginationProps | boolean => {
|
||||||
return unref(getPaginationInfo);
|
const { pagination } = unref(refProps);
|
||||||
|
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
page: 1, //当前页
|
||||||
|
pageSize: DEFAULTPAGESIZE, //分页大小
|
||||||
|
pageSizes: PAGESIZES, // 每页条数
|
||||||
|
showSizePicker: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
prefix: (pagingInfo) => `共 ${pagingInfo.itemCount} 条`, // 不需要可以通过 pagination 重置或者删除
|
||||||
|
...(isBoolean(pagination) ? {} : pagination),
|
||||||
|
...unref(configRef),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function getShowPagination() {
|
function setPagination(info: Partial<PaginationProps>) {
|
||||||
return unref(show);
|
const paginationInfo = unref(getPaginationInfo);
|
||||||
}
|
configRef.value = {
|
||||||
|
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
|
||||||
|
...info,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function setShowPagination(flag: boolean) {
|
function getPagination() {
|
||||||
show.value = flag;
|
return unref(getPaginationInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
|
function getShowPagination() {
|
||||||
|
return unref(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setShowPagination(flag: boolean) {
|
||||||
|
show.value = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import { provide, inject, ComputedRef } from 'vue';
|
|||||||
const key = Symbol('s-table');
|
const key = Symbol('s-table');
|
||||||
|
|
||||||
type Instance = TableActionType & {
|
type Instance = TableActionType & {
|
||||||
wrapRef: Ref<Nullable<HTMLElement>>;
|
wrapRef: Ref<Nullable<HTMLElement>>;
|
||||||
getBindValues: ComputedRef<Recordable>;
|
getBindValues: ComputedRef<Recordable>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RetInstance = Omit<Instance, 'getBindValues'> & {
|
type RetInstance = Omit<Instance, 'getBindValues'> & {
|
||||||
getBindValues: ComputedRef<BasicTableProps>;
|
getBindValues: ComputedRef<BasicTableProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createTableContext(instance: Instance) {
|
export function createTableContext(instance: Instance) {
|
||||||
provide(key, instance);
|
provide(key, instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTableContext(): RetInstance {
|
export function useTableContext(): RetInstance {
|
||||||
return inject(key) as RetInstance;
|
return inject(key) as RetInstance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,60 @@
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue';
|
||||||
import { BasicColumn } from './types/table'
|
import { propTypes } from '@/utils/propTypes';
|
||||||
|
import { BasicColumn } from './types/table';
|
||||||
|
import { NDataTable } from 'naive-ui';
|
||||||
export const basicProps = {
|
export const basicProps = {
|
||||||
title: {
|
...NDataTable.props, // 这里继承原 UI 组件的 props
|
||||||
type: String,
|
title: {
|
||||||
default: null,
|
type: String,
|
||||||
},
|
default: null,
|
||||||
titleTooltip: {
|
},
|
||||||
type: String,
|
titleTooltip: {
|
||||||
default: null,
|
type: String,
|
||||||
},
|
default: null,
|
||||||
size: {
|
},
|
||||||
type: String,
|
size: {
|
||||||
default: 'medium',
|
type: String,
|
||||||
},
|
default: 'medium',
|
||||||
tableData: {
|
},
|
||||||
type: [Object],
|
dataSource: {
|
||||||
default: () => {
|
type: [Object],
|
||||||
},
|
default: () => [],
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
type: [Array] as PropType<BasicColumn[]>,
|
type: [Array] as PropType<BasicColumn[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
request: {
|
beforeRequest: {
|
||||||
type: Function as PropType<(...arg: any[]) => Promise<any>>,
|
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||||
default: null,
|
default: null,
|
||||||
required: true
|
},
|
||||||
},
|
request: {
|
||||||
rowKey: {
|
type: Function as PropType<(...arg: any[]) => Promise<any>>,
|
||||||
type: [String, Function] as PropType<string | ((record) => string)>,
|
default: null,
|
||||||
default: undefined,
|
},
|
||||||
},
|
afterRequest: {
|
||||||
pagination: {
|
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||||
type: [Object, Boolean],
|
default: null,
|
||||||
default: () => {
|
},
|
||||||
}
|
rowKey: {
|
||||||
},
|
type: [String, Function] as PropType<string | ((record) => string)>,
|
||||||
showPagination: {
|
default: undefined,
|
||||||
type: [String, Boolean],
|
},
|
||||||
default: 'auto'
|
pagination: {
|
||||||
},
|
type: [Object, Boolean],
|
||||||
actionColumn: {
|
default: () => {},
|
||||||
type: Object as PropType<BasicColumn>,
|
},
|
||||||
default: null,
|
//废弃
|
||||||
},
|
showPagination: {
|
||||||
}
|
type: [String, Boolean],
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
actionColumn: {
|
||||||
|
type: Object as PropType<BasicColumn>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
canResize: propTypes.bool.def(true),
|
||||||
|
resizeHeightOffset: propTypes.number.def(0),
|
||||||
|
striped: propTypes.bool.def(false),
|
||||||
|
};
|
||||||
|
|||||||
9
src/components/Table/src/types/componentType.ts
Normal file
9
src/components/Table/src/types/componentType.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export type ComponentType =
|
||||||
|
| 'NInput'
|
||||||
|
| 'NInputNumber'
|
||||||
|
| 'NSelect'
|
||||||
|
| 'NCheckbox'
|
||||||
|
| 'NSwitch'
|
||||||
|
| 'NDatePicker'
|
||||||
|
| 'NTimePicker'
|
||||||
|
| 'NCascader';
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import Pagination from 'naive-ui/lib/pagination';
|
|
||||||
import { VNodeChild } from 'vue';
|
|
||||||
|
|
||||||
export interface PaginationProps {
|
export interface PaginationProps {
|
||||||
page?: number;
|
page?: number; //受控模式下的当前页
|
||||||
pageCount?: number,
|
itemCount?: number; //总条数
|
||||||
pageSize?: number,
|
pageCount?: number; //总页数
|
||||||
pageSizes?: number[],
|
pageSize?: number; //受控模式下的分页大小
|
||||||
showSizePicker?: boolean,
|
pageSizes?: number[]; //每页条数, 可自定义
|
||||||
showQuickJumper?: boolean,
|
showSizePicker?: boolean; //是否显示每页条数的选择器
|
||||||
|
showQuickJumper?: boolean; //是否显示快速跳转
|
||||||
|
prefix?: any; //分页前缀
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
import type {
|
import type { InternalRowData, TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
|
||||||
TableBaseColumn,
|
import { ComponentType } from './componentType';
|
||||||
} from 'naive-ui/lib/data-table/src/interface';
|
export interface BasicColumn<T = InternalRowData> extends TableBaseColumn<T> {
|
||||||
|
//编辑表格
|
||||||
export interface BasicColumn extends TableBaseColumn {
|
edit?: boolean;
|
||||||
|
editRow?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
editComponent?: ComponentType;
|
||||||
|
editComponentProps?: Recordable;
|
||||||
|
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
|
||||||
|
editValueMap?: (value: any) => string;
|
||||||
|
onEditRow?: () => void;
|
||||||
|
// 权限编码控制是否显示
|
||||||
|
auth?: string[];
|
||||||
|
// 业务控制是否显示
|
||||||
|
ifShow?: boolean | ((column: BasicColumn) => boolean);
|
||||||
|
// 控制是否支持拖拽,默认支持
|
||||||
|
draggable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableActionType {
|
export interface TableActionType {
|
||||||
reload: (opt) => Promise<void>;
|
reload: (opt) => Promise<void>;
|
||||||
emit?: any;
|
emit?: any;
|
||||||
getColumns: (opt) => BasicColumn[];
|
getColumns: (opt?) => BasicColumn[];
|
||||||
setColumns: (columns: BasicColumn[] | string[]) => void;
|
setColumns: (columns: BasicColumn[] | string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BasicTableProps<T = any> {
|
export interface BasicTableProps {
|
||||||
title?: string,
|
title?: string;
|
||||||
dataSource: Function,
|
dataSource: Function;
|
||||||
columns: any[],
|
columns: any[];
|
||||||
pagination: object,
|
pagination: object;
|
||||||
showPagination: boolean
|
showPagination: boolean;
|
||||||
|
actionColumn: any[];
|
||||||
|
canResize: boolean;
|
||||||
|
resizeHeightOffset: number;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import { NButton, NTooltip } from 'naive-ui';
|
import { NButton } from 'naive-ui';
|
||||||
import { RoleEnum } from '/@/enums/roleEnum';
|
import type { Component } from 'vue';
|
||||||
|
import { PermissionsEnum } from '@/enums/permissionsEnum';
|
||||||
export interface ActionItem extends NButton.props {
|
export interface ActionItem extends Partial<InstanceType<typeof NButton>> {
|
||||||
onClick?: Fn;
|
onClick?: Fn;
|
||||||
label?: string;
|
label?: string;
|
||||||
color?: 'success' | 'error' | 'warning';
|
type?: 'success' | 'error' | 'warning' | 'info' | 'primary' | 'default';
|
||||||
icon?: string;
|
// 设定 color 后会覆盖 type 的样式
|
||||||
popConfirm?: PopConfirm;
|
color?: string;
|
||||||
disabled?: boolean;
|
icon?: Component;
|
||||||
divider?: boolean;
|
popConfirm?: PopConfirm;
|
||||||
// 权限编码控制是否显示
|
disabled?: boolean;
|
||||||
auth?: RoleEnum | RoleEnum[] | string | string[];
|
divider?: boolean;
|
||||||
// 业务控制是否显示
|
// 权限编码控制是否显示
|
||||||
ifShow?: boolean | ((action: ActionItem) => boolean);
|
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
|
||||||
tooltip?: string | TooltipProps;
|
// 业务控制是否显示
|
||||||
|
ifShow?: boolean | ((action: ActionItem) => boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PopConfirm {
|
export interface PopConfirm {
|
||||||
title: string;
|
title: string;
|
||||||
okText?: string;
|
okText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
confirm: Fn;
|
confirm: Fn;
|
||||||
cancel?: Fn;
|
cancel?: Fn;
|
||||||
icon?: string;
|
icon?: Component;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,49 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="upload">
|
<div class="upload">
|
||||||
<div class="upload-card">
|
<div class="upload-card">
|
||||||
|
|
||||||
<!--图片列表-->
|
<!--图片列表-->
|
||||||
<div class="upload-card-item" :style="getCSSProperties" v-for="(item,index) in imgList">
|
<div
|
||||||
|
class="upload-card-item"
|
||||||
|
:style="getCSSProperties"
|
||||||
|
v-for="(item, index) in imgList"
|
||||||
|
:key="`img_${index}`"
|
||||||
|
>
|
||||||
<div class="upload-card-item-info">
|
<div class="upload-card-item-info">
|
||||||
<div class="img-box">
|
<div class="img-box">
|
||||||
<img :src="item"/>
|
<img :src="item" />
|
||||||
</div>
|
</div>
|
||||||
<div class="img-box-actions">
|
<div class="img-box-actions">
|
||||||
<n-icon size="18" class="action-icon mx-2" @click="preview(item)">
|
<n-icon size="18" class="mx-2 action-icon" @click="preview(item)">
|
||||||
<EyeOutlined/>
|
<EyeOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
<n-icon size="18" class="action-icon mx-2" @click="remove(index)">
|
<n-icon size="18" class="mx-2 action-icon" @click="remove(index)">
|
||||||
<DeleteOutlined/>
|
<DeleteOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--上传图片-->
|
<!--上传图片-->
|
||||||
<div class="upload-card-item upload-card-item-select-picture" :style="getCSSProperties"
|
<div
|
||||||
v-if="imgList.length < maxNumber">
|
class="upload-card-item upload-card-item-select-picture"
|
||||||
|
:style="getCSSProperties"
|
||||||
|
v-if="imgList.length < maxNumber"
|
||||||
|
>
|
||||||
<n-upload
|
<n-upload
|
||||||
v-bind="$props"
|
class="w-auto"
|
||||||
:file-list-style="{ display:'none'}"
|
v-bind="$props"
|
||||||
@before-upload="beforeUpload"
|
:file-list-style="{ display: 'none' }"
|
||||||
@finish="finish"
|
@before-upload="beforeUpload"
|
||||||
|
@finish="finish"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center flex-col">
|
<div class="flex flex-col justify-center">
|
||||||
<n-icon size="18" class="m-auto">
|
<n-icon size="18" class="m-auto">
|
||||||
<PlusOutlined/>
|
<PlusOutlined />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
<span class="upload-title">上传图片</span>
|
<span class="upload-title">上传图片</span>
|
||||||
</div>
|
</div>
|
||||||
</n-upload>
|
</n-upload>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,267 +54,258 @@
|
|||||||
{{ helpText }}
|
{{ helpText }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--预览图片-->
|
<!--预览图片-->
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="showModal"
|
v-model:show="showModal"
|
||||||
preset="card"
|
preset="card"
|
||||||
title="预览"
|
title="预览"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
:style="{width: '520px'}"
|
:style="{ width: '520px' }"
|
||||||
>
|
>
|
||||||
<img :src="previewUrl"/>
|
<img :src="previewUrl" />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, toRefs, reactive, computed } from "vue";
|
import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
|
||||||
import { EyeOutlined, DeleteOutlined, PlusOutlined } from "@vicons/antd";
|
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
|
||||||
import { NUpload } from 'naive-ui'
|
import { basicProps } from './props';
|
||||||
import { basicProps } from "./props";
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
import { useMessage, useDialog } from 'naive-ui'
|
import { ResultEnum } from '@/enums/httpEnum';
|
||||||
import { ResultEnum } from '@/enums/httpEnum'
|
import componentSetting from '@/settings/componentSetting';
|
||||||
import componentSetting from '@/settings/componentSetting'
|
import { useGlobSetting } from '@/hooks/setting';
|
||||||
import { useGlobSetting } from '@/hooks/setting'
|
import { isString } from '@/utils/is';
|
||||||
import { isString } from '@/utils/is'
|
|
||||||
|
|
||||||
const globSetting = useGlobSetting()
|
const globSetting = useGlobSetting();
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'BasicUpload',
|
name: 'BasicUpload',
|
||||||
|
|
||||||
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
|
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
|
||||||
props: {
|
props: {
|
||||||
...NUpload.props, // 这里继承原 UI 组件的 props
|
...basicProps,
|
||||||
...basicProps
|
},
|
||||||
},
|
emits: ['uploadChange', 'delete'],
|
||||||
emits: ['uploadChange', 'delete'],
|
setup(props, { emit }) {
|
||||||
setup(props, { emit }) {
|
const getCSSProperties = computed(() => {
|
||||||
const { value, width, height } = props
|
return {
|
||||||
|
width: `${props.width}px`,
|
||||||
|
height: `${props.height}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const getCSSProperties = computed(() => {
|
const message = useMessage();
|
||||||
return {
|
const dialog = useDialog();
|
||||||
width: `${ width }px`,
|
|
||||||
height: `${ height }px`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const message = useMessage()
|
const state = reactive({
|
||||||
const dialog = useDialog()
|
showModal: false,
|
||||||
|
previewUrl: '',
|
||||||
|
originalImgList: [] as string[],
|
||||||
|
imgList: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
const state = reactive({
|
//赋值默认图片显示
|
||||||
showModal: false,
|
watch(
|
||||||
previewUrl: '',
|
() => props.value,
|
||||||
originalImgList: [],
|
() => {
|
||||||
imgList: []
|
state.imgList = props.value.map((item) => {
|
||||||
})
|
return getImgUrl(item);
|
||||||
|
});
|
||||||
//赋值默认图片显示
|
|
||||||
if (value.length) {
|
|
||||||
state.imgList = value.map(item => {
|
|
||||||
return getImgUrl(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//预览
|
|
||||||
function preview(url: string) {
|
|
||||||
state.showModal = true
|
|
||||||
state.previewUrl = url
|
|
||||||
}
|
|
||||||
|
|
||||||
//删除
|
|
||||||
function remove(index: number) {
|
|
||||||
dialog.info({
|
|
||||||
title: '提示',
|
|
||||||
content: '你确定要删除吗?',
|
|
||||||
positiveText: '确定',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: () => {
|
|
||||||
state.imgList.splice(index, 1)
|
|
||||||
state.originalImgList.splice(index, 1)
|
|
||||||
emit('uploadChange', state.originalImgList)
|
|
||||||
emit('delete', state.originalImgList)
|
|
||||||
},
|
},
|
||||||
onNegativeClick: () => {
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
//预览
|
||||||
|
function preview(url: string) {
|
||||||
|
state.showModal = true;
|
||||||
|
state.previewUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
//删除
|
||||||
|
function remove(index: number) {
|
||||||
|
dialog.info({
|
||||||
|
title: '提示',
|
||||||
|
content: '你确定要删除吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
state.imgList.splice(index, 1);
|
||||||
|
state.originalImgList.splice(index, 1);
|
||||||
|
emit('uploadChange', state.originalImgList);
|
||||||
|
emit('delete', state.originalImgList);
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//组装完整图片地址
|
||||||
|
function getImgUrl(url: string): string {
|
||||||
|
const { imgUrl } = globSetting;
|
||||||
|
return /(^http|https:\/\/)/g.test(url) ? url : `${imgUrl}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFileType(fileType: string) {
|
||||||
|
return componentSetting.upload.fileType.includes(fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
//上传之前
|
||||||
|
function beforeUpload({ file }) {
|
||||||
|
const fileInfo = file.file;
|
||||||
|
const { maxSize, accept } = props;
|
||||||
|
const acceptRef = (isString(accept) && accept.split(',')) || [];
|
||||||
|
|
||||||
|
// 设置最大值,则判断
|
||||||
|
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
|
||||||
|
message.error(`上传文件最大值不能超过${maxSize}M`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//组装完整图片地址
|
// 设置类型,则判断
|
||||||
function getImgUrl(url: string) {
|
const fileType = componentSetting.upload.fileType;
|
||||||
const { imgUrl } = globSetting
|
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
|
||||||
return (/(^http|https:\/\/)/g).test(url) ? url : `${ imgUrl }${ url }`
|
message.error(`只能上传文件类型为${fileType.join(',')}`);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
function checkFileType(fileType: string) {
|
|
||||||
return componentSetting.upload.fileType.includes(fileType)
|
|
||||||
}
|
|
||||||
|
|
||||||
//上传之前
|
|
||||||
function beforeUpload({ file, fileList }) {
|
|
||||||
const fileInfo = file.file;
|
|
||||||
const { maxSize, accept, maxNumber } = props;
|
|
||||||
const acceptRef = (isString(accept) && accept.split(',')) || [];
|
|
||||||
|
|
||||||
// 设置最大值,则判断
|
|
||||||
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
|
|
||||||
message.error(`上传文件最大值不能超过${ maxSize }M`);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置类型,则判断
|
//上传结束
|
||||||
const fileType = componentSetting.upload.fileType
|
function finish({ event: Event }) {
|
||||||
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
|
const res = eval('(' + Event.target.response + ')');
|
||||||
message.error(`只能上传文件类型为${ fileType.join(',') }`);
|
const infoField = componentSetting.upload.apiSetting.infoField;
|
||||||
return false;
|
const { code } = res;
|
||||||
|
const message = res.msg || res.message || '上传失败';
|
||||||
|
const result = res[infoField];
|
||||||
|
//成功
|
||||||
|
if (code === ResultEnum.SUCCESS) {
|
||||||
|
let imgUrl: string = getImgUrl(result.photo);
|
||||||
|
state.imgList.push(imgUrl);
|
||||||
|
state.originalImgList.push(result.photo);
|
||||||
|
emit('uploadChange', state.originalImgList);
|
||||||
|
} else message.error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return {
|
||||||
}
|
...toRefs(state),
|
||||||
|
finish,
|
||||||
//上传结束
|
preview,
|
||||||
function finish({ event: Event }) {
|
remove,
|
||||||
const res = eval('(' + Event.target.response + ')');
|
beforeUpload,
|
||||||
const infoField = componentSetting.upload.apiSetting.infoField
|
getCSSProperties,
|
||||||
const { code } = res
|
};
|
||||||
const message = (res.msg || res.message) || '上传失败'
|
},
|
||||||
const result = res[infoField]
|
});
|
||||||
//成功
|
|
||||||
if (code === ResultEnum.SUCCESS) {
|
|
||||||
let imgUrl: string = getImgUrl(result.photo)
|
|
||||||
state.imgList.push(imgUrl)
|
|
||||||
state.originalImgList.push(result.photo)
|
|
||||||
emit('uploadChange', state.originalImgList)
|
|
||||||
} else message.error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
finish,
|
|
||||||
preview,
|
|
||||||
remove,
|
|
||||||
beforeUpload,
|
|
||||||
getCSSProperties
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
.upload {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.upload {
|
&-card {
|
||||||
width: 100%;
|
width: auto;
|
||||||
overflow: hidden;
|
height: auto;
|
||||||
|
|
||||||
&-card {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&-item {
|
|
||||||
margin: 0 8px 8px 0;
|
|
||||||
position: relative;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 2px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-wrap: wrap;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&:hover {
|
&-item {
|
||||||
background: 0 0;
|
margin: 0 8px 8px 0;
|
||||||
|
|
||||||
&-info::before {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
padding: 8px;
|
||||||
padding: 0;
|
border: 1px solid #d9d9d9;
|
||||||
overflow: hidden;
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.img-box-actions {
|
background: 0 0;
|
||||||
|
|
||||||
|
.upload-card-item-info::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-info::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&-info {
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, .5);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all .3s;
|
|
||||||
content: ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-box {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
//padding: 8px;
|
height: 100%;
|
||||||
//border: 1px solid #d9d9d9;
|
width: 100%;
|
||||||
border-radius: 2px;
|
padding: 0;
|
||||||
}
|
overflow: hidden;
|
||||||
|
|
||||||
.img-box-actions {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
z-index: 10;
|
|
||||||
white-space: nowrap;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all .3s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: 0 0;
|
.img-box-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-icon {
|
&::before {
|
||||||
color: rgba(255, 255, 255, .85);
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-box {
|
||||||
|
position: relative;
|
||||||
|
//padding: 8px;
|
||||||
|
//border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-box-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
background: 0 0;
|
||||||
color: #fff
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
&-item-select-picture {
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
&-item-select-picture {
|
border-radius: 2px;
|
||||||
border: 1px dashed #d9d9d9;
|
cursor: pointer;
|
||||||
border-radius: 2px;
|
background: #fafafa;
|
||||||
cursor: pointer;
|
|
||||||
background: #fafafa;
|
|
||||||
color: #666;
|
|
||||||
|
|
||||||
.upload-title {
|
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-item:hover {
|
.upload-title {
|
||||||
background: 0 0;
|
color: #666;
|
||||||
|
}
|
||||||
.upload-card-item-info::before {
|
|
||||||
opacity: 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue';
|
||||||
import { NUpload } from 'naive-ui'
|
import { NUpload } from 'naive-ui';
|
||||||
|
|
||||||
export const basicProps = {
|
export const basicProps = {
|
||||||
...NUpload.props,
|
...NUpload.props,
|
||||||
accept: {
|
accept: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '.jpg,.png,.jpeg,.svg,.gif',
|
default: '.jpg,.png,.jpeg,.svg,.gif',
|
||||||
},
|
},
|
||||||
helpText: {
|
helpText: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<string>,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
maxSize: {
|
maxSize: {
|
||||||
type: Number as PropType<number>,
|
type: Number as PropType<number>,
|
||||||
default: 2
|
default: 2,
|
||||||
},
|
},
|
||||||
maxNumber: {
|
maxNumber: {
|
||||||
type: Number as PropType<number>,
|
type: Number as PropType<number>,
|
||||||
default: Infinity
|
default: Infinity,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<string[]>,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: Number as PropType<number>,
|
type: Number as PropType<number>,
|
||||||
default: 104
|
default: 104,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: Number as PropType<number>,
|
type: Number as PropType<number>,
|
||||||
default: 104 //建议不小于这个尺寸 太小页面可能显示有异常
|
default: 104, //建议不小于这个尺寸 太小页面可能显示有异常
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export interface BasicProps<T = any> {
|
export interface BasicProps {
|
||||||
title?: string,
|
title?: string;
|
||||||
dataSource: Function,
|
dataSource: Function;
|
||||||
columns: any[],
|
columns: any[];
|
||||||
pagination: object,
|
pagination: object;
|
||||||
showPagination: boolean
|
showPagination: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/config/website.config.ts
Normal file
9
src/config/website.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import logoImage from '@/assets/images/logo.png';
|
||||||
|
import loginImage from '@/assets/images/account-logo.png';
|
||||||
|
|
||||||
|
export const websiteConfig = Object.freeze({
|
||||||
|
title: 'NaiveUiAdmin',
|
||||||
|
logo: logoImage,
|
||||||
|
loginImage: loginImage,
|
||||||
|
loginDesc: 'Naive Ui Admin 中后台前端/设计解决方案',
|
||||||
|
});
|
||||||
86
src/directives/clickOutside.ts
Normal file
86
src/directives/clickOutside.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { on } from '@/utils/domUtils';
|
||||||
|
import { isServer } from '@/utils/is';
|
||||||
|
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
|
||||||
|
|
||||||
|
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
|
||||||
|
|
||||||
|
type FlushList = Map<
|
||||||
|
HTMLElement,
|
||||||
|
{
|
||||||
|
documentHandler: DocumentHandler;
|
||||||
|
bindingFn: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const nodeList: FlushList = new Map();
|
||||||
|
|
||||||
|
let startClick: MouseEvent;
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
|
||||||
|
on(document, 'mouseup', (e: MouseEvent) => {
|
||||||
|
for (const { documentHandler } of nodeList.values()) {
|
||||||
|
documentHandler(e, startClick);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
|
||||||
|
let excludes: HTMLElement[] = [];
|
||||||
|
if (Array.isArray(binding.arg)) {
|
||||||
|
excludes = binding.arg;
|
||||||
|
} else {
|
||||||
|
// due to current implementation on binding type is wrong the type casting is necessary here
|
||||||
|
excludes.push(binding.arg as unknown as HTMLElement);
|
||||||
|
}
|
||||||
|
return function (mouseup, mousedown) {
|
||||||
|
const popperRef = (
|
||||||
|
binding.instance as ComponentPublicInstance<{
|
||||||
|
popperRef: Nullable<HTMLElement>;
|
||||||
|
}>
|
||||||
|
).popperRef;
|
||||||
|
const mouseUpTarget = mouseup.target as Node;
|
||||||
|
const mouseDownTarget = mousedown.target as Node;
|
||||||
|
const isBound = !binding || !binding.instance;
|
||||||
|
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
|
||||||
|
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
|
||||||
|
const isSelf = el === mouseUpTarget;
|
||||||
|
|
||||||
|
const isTargetExcluded =
|
||||||
|
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
|
||||||
|
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
|
||||||
|
const isContainedByPopper =
|
||||||
|
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
|
||||||
|
if (
|
||||||
|
isBound ||
|
||||||
|
isTargetExists ||
|
||||||
|
isContainedByEl ||
|
||||||
|
isSelf ||
|
||||||
|
isTargetExcluded ||
|
||||||
|
isContainedByPopper
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
binding.value();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClickOutside: ObjectDirective = {
|
||||||
|
beforeMount(el, binding) {
|
||||||
|
nodeList.set(el, {
|
||||||
|
documentHandler: createDocumentHandler(el, binding),
|
||||||
|
bindingFn: binding.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
nodeList.set(el, {
|
||||||
|
documentHandler: createDocumentHandler(el, binding),
|
||||||
|
bindingFn: binding.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
unmounted(el) {
|
||||||
|
nodeList.delete(el);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClickOutside;
|
||||||
34
src/directives/copy.ts
Normal file
34
src/directives/copy.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* v-copy
|
||||||
|
* 复制某个值至剪贴板
|
||||||
|
* 接收参数:string类型/Ref<string>类型/Reactive<string>类型
|
||||||
|
*/
|
||||||
|
import type { Directive, DirectiveBinding } from 'vue';
|
||||||
|
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
copyData: string | number;
|
||||||
|
__handleClick__: any;
|
||||||
|
}
|
||||||
|
const copy: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
el.copyData = binding.value;
|
||||||
|
el.addEventListener('click', handleClick);
|
||||||
|
},
|
||||||
|
updated(el: ElType, binding: DirectiveBinding) {
|
||||||
|
el.copyData = binding.value;
|
||||||
|
},
|
||||||
|
beforeUnmount(el: ElType) {
|
||||||
|
el.removeEventListener('click', el.__handleClick__);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function handleClick(this: any) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.value = this.copyData.toLocaleString();
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.select();
|
||||||
|
document.execCommand('Copy');
|
||||||
|
document.body.removeChild(input);
|
||||||
|
console.log('复制成功', this.copyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default copy;
|
||||||
31
src/directives/debounce.ts
Normal file
31
src/directives/debounce.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* v-debounce
|
||||||
|
* 按钮防抖指令,可自行扩展至input
|
||||||
|
* 接收参数:function类型
|
||||||
|
*/
|
||||||
|
import type { Directive, DirectiveBinding } from 'vue';
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
__handleClick__: () => any;
|
||||||
|
}
|
||||||
|
const debounce: Directive = {
|
||||||
|
mounted(el: ElType, binding: DirectiveBinding) {
|
||||||
|
if (typeof binding.value !== 'function') {
|
||||||
|
throw 'callback must be a function';
|
||||||
|
}
|
||||||
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
el.__handleClick__ = function () {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
binding.value();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
el.addEventListener('click', el.__handleClick__);
|
||||||
|
},
|
||||||
|
beforeUnmount(el: ElType) {
|
||||||
|
el.removeEventListener('click', el.__handleClick__);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default debounce;
|
||||||
49
src/directives/draggable.ts
Normal file
49
src/directives/draggable.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
|
||||||
|
|
||||||
|
思路:
|
||||||
|
1、设置需要拖拽的元素为absolute,其父元素为relative。
|
||||||
|
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
|
||||||
|
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
|
||||||
|
4、鼠标松开(onmouseup)时完成一次拖拽
|
||||||
|
|
||||||
|
使用:在 Dom 上加上 v-draggable 即可
|
||||||
|
<div class="dialog-model" v-draggable></div>
|
||||||
|
*/
|
||||||
|
import type { Directive } from 'vue';
|
||||||
|
interface ElType extends HTMLElement {
|
||||||
|
parentNode: any;
|
||||||
|
}
|
||||||
|
const draggable: Directive = {
|
||||||
|
mounted: function (el: ElType) {
|
||||||
|
el.style.cursor = 'move';
|
||||||
|
el.style.position = 'absolute';
|
||||||
|
el.onmousedown = function (e) {
|
||||||
|
const disX = e.pageX - el.offsetLeft;
|
||||||
|
const disY = e.pageY - el.offsetTop;
|
||||||
|
document.onmousemove = function (e) {
|
||||||
|
let x = e.pageX - disX;
|
||||||
|
let y = e.pageY - disY;
|
||||||
|
const maxX = el.parentNode.offsetWidth - el.offsetWidth;
|
||||||
|
const maxY = el.parentNode.offsetHeight - el.offsetHeight;
|
||||||
|
if (x < 0) {
|
||||||
|
x = 0;
|
||||||
|
} else if (x > maxX) {
|
||||||
|
x = maxX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
} else if (y > maxY) {
|
||||||
|
y = maxY;
|
||||||
|
}
|
||||||
|
el.style.left = x + 'px';
|
||||||
|
el.style.top = y + 'px';
|
||||||
|
};
|
||||||
|
document.onmouseup = function () {
|
||||||
|
document.onmousemove = document.onmouseup = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default draggable;
|
||||||
49
src/directives/longpress.ts
Normal file
49
src/directives/longpress.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* v-longpress
|
||||||
|
* 长按指令,长按时触发事件
|
||||||
|
*/
|
||||||
|
import type { Directive, DirectiveBinding } from 'vue';
|
||||||
|
|
||||||
|
const directive: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
if (typeof binding.value !== 'function') {
|
||||||
|
throw 'callback must be a function';
|
||||||
|
}
|
||||||
|
// 定义变量
|
||||||
|
let pressTimer: any = null;
|
||||||
|
// 创建计时器( 2秒后执行函数 )
|
||||||
|
const start = (e: any) => {
|
||||||
|
if (e.button) {
|
||||||
|
if (e.type === 'click' && e.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pressTimer === null) {
|
||||||
|
pressTimer = setTimeout(() => {
|
||||||
|
handler(e);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 取消计时器
|
||||||
|
const cancel = () => {
|
||||||
|
if (pressTimer !== null) {
|
||||||
|
clearTimeout(pressTimer);
|
||||||
|
pressTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 运行函数
|
||||||
|
const handler = (e: MouseEvent | TouchEvent) => {
|
||||||
|
binding.value(e);
|
||||||
|
};
|
||||||
|
// 添加事件监听器
|
||||||
|
el.addEventListener('mousedown', start);
|
||||||
|
el.addEventListener('touchstart', start);
|
||||||
|
// 取消计时器
|
||||||
|
el.addEventListener('click', cancel);
|
||||||
|
el.addEventListener('mouseout', cancel);
|
||||||
|
el.addEventListener('touchend', cancel);
|
||||||
|
el.addEventListener('touchcancel', cancel);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default directive;
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { ObjectDirective } from 'vue'
|
import { ObjectDirective } from 'vue';
|
||||||
import { usePermission } from "@/hooks/web/usePermission";
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
|
|
||||||
export const permission: ObjectDirective = {
|
export const permission: ObjectDirective = {
|
||||||
mounted(el: HTMLButtonElement, binding, vnode) {
|
mounted(el: HTMLButtonElement, binding) {
|
||||||
if (binding.value == undefined) return
|
if (binding.value == undefined) return;
|
||||||
const { action, effect } = binding.value
|
const { action, effect } = binding.value;
|
||||||
const { hasPermission } = usePermission()
|
const { hasPermission } = usePermission();
|
||||||
if (!hasPermission(action)) {
|
if (!hasPermission(action)) {
|
||||||
if (effect == 'disabled') {
|
if (effect == 'disabled') {
|
||||||
el.disabled = true
|
el.disabled = true;
|
||||||
el.style["disabled"] = 'disabled'
|
el.style['disabled'] = 'disabled';
|
||||||
el.classList.add("n-button--disabled")
|
el.classList.add('n-button--disabled');
|
||||||
} else {
|
} else {
|
||||||
el.remove()
|
el.remove();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user