Merge branch 'main' of github.com:maxkb-dev/maxkb

This commit is contained in:
shaohuzhang1 2023-10-24 20:24:57 +08:00
commit dc934c2f61
74 changed files with 2654 additions and 1742 deletions

3
ui/env/.env vendored
View File

@ -1,3 +1,4 @@
VITE_APP_NAME=ui
VITE_BASE_PATH=/ui/
VITE_APP_PORT=3000
VITE_APP_PORT=3000
VITE_APP_TITLE = '智能知识库'

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>%VITE_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>

345
ui/package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^0.27.2",
"element-plus": "^2.3.7",
"element-plus": "^2.3.14",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
@ -34,6 +34,7 @@
"prettier": "^3.0.0",
"sass": "^1.66.1",
"typescript": "~5.1.6",
"unplugin-vue-define-options": "^1.3.18",
"vite": "^4.4.9",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8"
@ -48,6 +49,24 @@
"node": ">=0.10.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz",
@ -59,6 +78,20 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@ -290,6 +323,28 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.0.5",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
"integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz",
@ -332,6 +387,12 @@
"@types/chai": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.2.tgz",
"integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==",
"dev": true
},
"node_modules/@types/jsdom": {
"version": "21.1.2",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.2.tgz",
@ -714,6 +775,31 @@
"@volar/language-core": "1.10.1"
}
},
"node_modules/@vue-macros/common": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/@vue-macros/common/-/common-1.8.0.tgz",
"integrity": "sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.17",
"@rollup/pluginutils": "^5.0.4",
"@vue/compiler-sfc": "^3.3.4",
"ast-kit": "^0.11.2",
"local-pkg": "^0.4.3",
"magic-string-ast": "^0.3.0"
},
"engines": {
"node": ">=16.14.0"
},
"peerDependencies": {
"vue": "^2.7.0 || ^3.2.25"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
@ -1189,6 +1275,47 @@
"node": "*"
}
},
"node_modules/ast-kit": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-0.11.2.tgz",
"integrity": "sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.22.14",
"@rollup/pluginutils": "^5.0.4",
"pathe": "^1.1.1"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/ast-walker-scope": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/ast-walker-scope/-/ast-walker-scope-0.5.0.tgz",
"integrity": "sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.22.7",
"ast-kit": "^0.9.4"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/ast-walker-scope/node_modules/ast-kit": {
"version": "0.9.5",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-0.9.5.tgz",
"integrity": "sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.22.7",
"@rollup/pluginutils": "^5.0.2",
"pathe": "^1.1.1"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@ -1733,9 +1860,9 @@
}
},
"node_modules/element-plus": {
"version": "2.3.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.12.tgz",
"integrity": "sha512-fAWpbKCyt+l1dsqSNPOs/F/dBN4Wp5CGAyxbiS5zqDwI4q3QPM+LxLU2h3GUHMIBtMGCvmsG98j5HPMkTKkvcA==",
"version": "2.3.14",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.14.tgz",
"integrity": "sha512-9yvxUaU4jXf2ZNPdmIxoj/f8BG8CDcGM6oHa9JIqxLjQlfY4bpzR1E5CjNimnOX3rxO93w1TQ0jTVt0RSxh9kA==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6",
@ -1901,7 +2028,7 @@
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-10.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/escape-string-regexp": {
@ -3378,6 +3505,18 @@
"node": ">=12"
}
},
"node_modules/magic-string-ast": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/magic-string-ast/-/magic-string-ast-0.3.0.tgz",
"integrity": "sha512-0shqecEPgdFpnI3AP90epXyxZy9g6CRZ+SZ7BcqFwYmtFEnZ1jpevcV5HoyVnlDS9gCnc1UIg3Rsvp3Ci7r8OA==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.2"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
@ -4945,6 +5084,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -5152,6 +5300,32 @@
"node": ">= 4.0.0"
}
},
"node_modules/unplugin": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.5.0.tgz",
"integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==",
"dev": true,
"dependencies": {
"acorn": "^8.10.0",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.5.0"
}
},
"node_modules/unplugin-vue-define-options": {
"version": "1.3.18",
"resolved": "https://registry.npmmirror.com/unplugin-vue-define-options/-/unplugin-vue-define-options-1.3.18.tgz",
"integrity": "sha512-AaE10FCccfezT48yyYuUXdnTF9z8vQuXrlpNF5uQtq/AOD2pdkf38vnmJm8bJjpoqEkR6u72wNCJLZKXSUw+Og==",
"dev": true,
"dependencies": {
"@vue-macros/common": "1.8.0",
"ast-walker-scope": "^0.5.0",
"unplugin": "^1.4.0"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
@ -5480,6 +5654,21 @@
"node": ">=12"
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
@ -5648,11 +5837,34 @@
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
"dev": true
},
"@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/parser": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz",
"integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA=="
},
"@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
"@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
@ -5819,6 +6031,17 @@
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ=="
},
"@rollup/pluginutils": {
"version": "5.0.5",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
"integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
"dev": true,
"requires": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
}
},
"@rushstack/eslint-patch": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz",
@ -5858,6 +6081,12 @@
"@types/chai": "*"
}
},
"@types/estree": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.2.tgz",
"integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==",
"dev": true
},
"@types/jsdom": {
"version": "21.1.2",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.2.tgz",
@ -6120,6 +6349,20 @@
"@volar/language-core": "1.10.1"
}
},
"@vue-macros/common": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/@vue-macros/common/-/common-1.8.0.tgz",
"integrity": "sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==",
"dev": true,
"requires": {
"@babel/types": "^7.22.17",
"@rollup/pluginutils": "^5.0.4",
"@vue/compiler-sfc": "^3.3.4",
"ast-kit": "^0.11.2",
"local-pkg": "^0.4.3",
"magic-string-ast": "^0.3.0"
}
},
"@vue/compiler-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
@ -6466,6 +6709,40 @@
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"ast-kit": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-0.11.2.tgz",
"integrity": "sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==",
"dev": true,
"requires": {
"@babel/parser": "^7.22.14",
"@rollup/pluginutils": "^5.0.4",
"pathe": "^1.1.1"
}
},
"ast-walker-scope": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/ast-walker-scope/-/ast-walker-scope-0.5.0.tgz",
"integrity": "sha512-NsyHMxBh4dmdEHjBo1/TBZvCKxffmZxRYhmclfu0PP6Aftre47jOHYaYaNqJcV0bxihxFXhDkzLHUwHc0ocd0Q==",
"dev": true,
"requires": {
"@babel/parser": "^7.22.7",
"ast-kit": "^0.9.4"
},
"dependencies": {
"ast-kit": {
"version": "0.9.5",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-0.9.5.tgz",
"integrity": "sha512-kbL7ERlqjXubdDd+szuwdlQ1xUxEz9mCz1+m07ftNVStgwRb2RWw+U6oKo08PAvOishMxiqz1mlJyLl8yQx2Qg==",
"dev": true,
"requires": {
"@babel/parser": "^7.22.7",
"@rollup/pluginutils": "^5.0.2",
"pathe": "^1.1.1"
}
}
}
},
"async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@ -6869,9 +7146,9 @@
}
},
"element-plus": {
"version": "2.3.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.12.tgz",
"integrity": "sha512-fAWpbKCyt+l1dsqSNPOs/F/dBN4Wp5CGAyxbiS5zqDwI4q3QPM+LxLU2h3GUHMIBtMGCvmsG98j5HPMkTKkvcA==",
"version": "2.3.14",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.14.tgz",
"integrity": "sha512-9yvxUaU4jXf2ZNPdmIxoj/f8BG8CDcGM6oHa9JIqxLjQlfY4bpzR1E5CjNimnOX3rxO93w1TQ0jTVt0RSxh9kA==",
"requires": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6",
@ -7006,7 +7283,7 @@
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-10.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-string-regexp": {
@ -8064,6 +8341,15 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"magic-string-ast": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/magic-string-ast/-/magic-string-ast-0.3.0.tgz",
"integrity": "sha512-0shqecEPgdFpnI3AP90epXyxZy9g6CRZ+SZ7BcqFwYmtFEnZ1jpevcV5HoyVnlDS9gCnc1UIg3Rsvp3Ci7r8OA==",
"dev": true,
"requires": {
"magic-string": "^0.30.2"
}
},
"memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
@ -9164,6 +9450,12 @@
"integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
"dev": true
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -9315,6 +9607,29 @@
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true
},
"unplugin": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.5.0.tgz",
"integrity": "sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==",
"dev": true,
"requires": {
"acorn": "^8.10.0",
"chokidar": "^3.5.3",
"webpack-sources": "^3.2.3",
"webpack-virtual-modules": "^0.5.0"
}
},
"unplugin-vue-define-options": {
"version": "1.3.18",
"resolved": "https://registry.npmmirror.com/unplugin-vue-define-options/-/unplugin-vue-define-options-1.3.18.tgz",
"integrity": "sha512-AaE10FCccfezT48yyYuUXdnTF9z8vQuXrlpNF5uQtq/AOD2pdkf38vnmJm8bJjpoqEkR6u72wNCJLZKXSUw+Og==",
"dev": true,
"requires": {
"@vue-macros/common": "1.8.0",
"ast-walker-scope": "^0.5.0",
"unplugin": "^1.4.0"
}
},
"untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
@ -9509,6 +9824,18 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true
},
"webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true
},
"webpack-virtual-modules": {
"version": "0.5.0",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true
},
"whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",

View File

@ -14,7 +14,7 @@
},
"dependencies": {
"axios": "^0.27.2",
"element-plus": "^2.3.7",
"element-plus": "^2.3.14",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
@ -39,6 +39,7 @@
"prettier": "^3.0.0",
"sass": "^1.66.1",
"typescript": "~5.1.6",
"unplugin-vue-define-options": "^1.3.18",
"vite": "^4.4.9",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8"

View File

@ -5,65 +5,5 @@
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

68
ui/src/api/team.ts Normal file
View File

@ -0,0 +1,68 @@
import { Result } from '@/request/Result'
import { get, post, del, put } from '@/request/index'
import type { TeamMember, TeamMemberRequest } from '@/api/type/team'
// import type { Ref } from 'vue'
const prefix = '/team/member'
/**
*
*/
const getTeamMember: () => Promise<Result<TeamMember[]>> = () => {
return get(`${prefix}`)
}
/**
*
* @param { "username_or_email": "string" }
*/
const postCreatTeamMember: (body: TeamMemberRequest) => Promise<Result<boolean>> = (body) => {
return post(`${prefix}`, body)
}
/**
*
* @param member_id
*/
const delTeamMember: (member_id: String) => Promise<Result<boolean>> = (member_id) => {
return del(`${prefix}/${member_id}`)
}
/**
*
* @param member_id
*/
const getMemberPermissions: (member_id: String) => Promise<Result<any>> = (member_id) => {
return get(`${prefix}/${member_id}`)
}
/**
*
* @param member_id
* @param {
"team_member_permission_list": [
{
"target_id": "string",
"type": "string",
"operate": {
"USE": true,
"MANAGE": true
}
}
]
}
*/
const putMemberPermissions: (member_id: String, body: any) => Promise<Result<any>> = (
member_id,
body
) => {
return put(`${prefix}/${member_id}`, undefined, body)
}
export default {
getTeamMember,
postCreatTeamMember,
delTeamMember,
getMemberPermissions,
putMemberPermissions
}

17
ui/src/api/type/team.ts Normal file
View File

@ -0,0 +1,17 @@
interface TeamMember {
id: string
username: string
email: string
team_id: string
/**
* typemanage
*/
type: string
user_id: string
}
interface TeamMemberRequest {
username_or_email: string
}
export type { TeamMember, TeamMemberRequest }

View File

@ -1,4 +1,4 @@
import { Result } from './../../request/Result'
import { Result } from '@/request/Result'
import { get, post } from '@/request/index'
import type {
LoginRequest,
@ -7,7 +7,7 @@ import type {
ResetPasswordRequest,
User,
ResetCurrentUserPasswordRequest
} from './type'
} from '@/api/type/user'
import type { Ref } from 'vue'
/**
@ -20,7 +20,7 @@ const login: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<s
request,
loading
) => {
return post('/user/login', undefined, request, loading)
return post('/user/login', request, undefined, loading)
}
/**
*
@ -41,7 +41,7 @@ const register: (request: RegisterRequest, loading?: Ref<boolean>) => Promise<Re
request,
loading
) => {
return post('/user/register', undefined, request, loading)
return post('/user/register', request, undefined, loading)
}
/**
@ -54,7 +54,7 @@ const checkCode: (request: CheckCodeRequest, loading?: Ref<boolean>) => Promise<
request,
loading
) => {
return post('/user/check_code', undefined, request, loading)
return post('/user/check_code', request, undefined, loading)
}
/**
@ -68,7 +68,7 @@ const sendEmit: (
type: 'register' | 'reset_password',
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (email, type, loading) => {
return post('/user/send_email', undefined, { email, type }, loading)
return post('/user/send_email', { email, type }, undefined, loading)
}
/**
*
@ -88,7 +88,7 @@ const resetCurrentUserPassword: (
request: ResetCurrentUserPasswordRequest,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (request, loading) => {
return post('/user/current/reset_password', undefined, request, loading)
return post('/user/current/reset_password', request, undefined, loading)
}
/**
*
@ -109,7 +109,7 @@ const resetPassword: (
request: ResetPasswordRequest,
loading?: Ref<boolean>
) => Promise<Result<boolean>> = (request, loading) => {
return post('/user/re_password', undefined, request, loading)
return post('/user/re_password', request, undefined, loading)
}
export default {

View File

@ -0,0 +1,42 @@
<template>
<div class="content-container">
<div class="content-container__header mb-10" v-if="slots.header || header">
<slot name="header">
<span>{{ header }}</span>
</slot>
</div>
<el-scrollbar>
<div class="content-container__main">
<slot></slot>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useSlots } from 'vue'
defineOptions({ name: 'LayoutContent' })
const slots = useSlots()
defineProps({
header: String
})
</script>
<style lang="scss" scope>
.content-container {
transition: 0.3s;
.content-container__header {
font-weight: 600;
font-size: 18px;
box-sizing: border-box;
}
.content-container__main {
background-color: var(--app-view-bg-color);
border-radius: 6px;
box-sizing: border-box;
// overflow: auto;
// height: 100%;
}
}
</style>

View File

@ -1,16 +1,33 @@
<template>
<component :is="Object.keys(iconMap).includes(iconName) ? iconMap[iconName].iconReader() : iconMap['404'].iconReader()">
<component
v-if="isIconfont"
:is="
Object.keys(iconMap).includes(iconName)
? iconMap[iconName].iconReader()
: iconMap['404'].iconReader()
"
class="el-icon app-icon"
>
</component>
<el-icon v-else-if="iconName">
<component :is="iconName" />
</el-icon>
</template>
<script setup lang="ts">
import { iconMap } from "@/components/icons/index"
withDefaults(defineProps<{
iconName?: string;
}>(), {
iconName: '404'
});
import { computed } from 'vue'
import { iconMap } from '@/components/icons/index'
defineOptions({ name: 'AppIcon' })
const props = withDefaults(
defineProps<{
iconName?: string
}>(),
{
iconName: '404'
}
)
const isIconfont = computed(() => props.iconName?.includes('app-'))
</script>
<style lang="scss" scoped></style>

View File

@ -1,8 +1,8 @@
import { h } from 'vue'
export const iconMap: any = {
'404': {
'app-404': {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
return h('i', [
h(
'svg',
{
@ -33,131 +33,102 @@ export const iconMap: any = {
])
}
},
home: {
'app-dataset': {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M362.666667 895.914667V639.850667c0-36.266667 33.109333-63.850667 72.533333-63.850667h153.6c39.253333 0 72.533333 27.648 72.533333 63.850667v256.064h59.904c61.269333 0 110.762667-47.957333 110.762667-106.730667V414.165333L557.162667 139.328a63.808 63.808 0 0 0-90.325334 0L192 414.165333v375.018667c0 58.88 49.386667 106.730667 110.762667 106.730667H362.666667z m42.666666 0h213.333334V639.850667c0-10.709333-12.586667-21.184-29.866667-21.184h-153.6c-17.408 0-29.866667 10.389333-29.866667 21.184v256.064z m469.333334-439.082667v332.352c0 82.645333-68.885333 149.397333-153.429334 149.397333H302.762667C218.133333 938.581333 149.333333 871.936 149.333333 789.184V456.832l-27.584 27.584a21.333333 21.333333 0 1 1-30.165333-30.165333L436.672 109.162667a106.474667 106.474667 0 0 1 150.656 0l345.088 345.088a21.333333 21.333333 0 0 1-30.165333 30.165333L874.666667 456.832z',
fill: '#666666'
})
]
)
])
}
},
app: {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M906.62890625 212.8203125C906.62890625 161.58007812 865.05664063 120.0078125 813.81640625 120.0078125H645.2421875C594.00195313 120.0078125 552.4296875 161.58007812 552.4296875 212.8203125v168.57421875c0 51.24023438 41.57226563 92.8125 92.8125 92.8125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V212.8203125z m-56.25 173.93554688c0 17.05078125-28.125 31.20117188-45.26367188 31.20117187H640.3203125c-17.05078125 0-30.76171875-14.0625-30.76171875-31.20117188V207.81054687c0-17.05078125 13.7109375-30.67382813 30.76171875-30.67382812h178.9453125c17.05078125 0 31.02539063 13.62304688 31.02539063 30.67382813v178.94531249z m56.25 251.45507812c0-51.24023438-41.57226563-92.8125-92.8125-92.8125H645.2421875C594.00195313 545.3984375 552.4296875 586.97070313 552.4296875 638.2109375v168.57421875c0 51.24023438 41.57226563 92.8125 92.8125 92.8125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V638.2109375z m-56.25 173.3203125c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H640.49609375c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V632.5859375c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125zM468.0546875 638.2109375c0-51.24023438-41.57226563-92.8125-92.8125-92.8125H206.66796875C155.42773437 545.3984375 113.85546875 586.97070313 113.85546875 638.2109375v168.57421875C113.85546875 858.02539063 155.42773437 899.59765625 206.66796875 899.59765625h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V638.2109375z m-57.12890625 173.3203125c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H201.04296875c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V632.5859375c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125z m57.12890625-598.7109375C468.0546875 161.58007812 426.48242187 120.0078125 375.2421875 120.0078125H206.66796875C155.42773437 120.0078125 113.85546875 161.58007812 113.85546875 212.8203125v168.57421875C113.85546875 432.63476562 155.42773437 474.20703125 206.66796875 474.20703125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V212.8203125z m-57.12890625 174.19921875c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H201.04296875c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V208.07421875c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125z',
fill: '#768696'
})
]
)
])
}
},
dataset: {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M859.5 193H446.939c-1.851-53.25-45.747-96-99.439-96h-183C109.635 97 65 141.635 65 196.5v632c0 54.864 44.635 99.5 99.5 99.5h695c54.864 0 99.5-44.636 99.5-99.5v-536c0-54.865-44.636-99.5-99.5-99.5z m-695-33h183c20.126 0 36.5 16.374 36.5 36.5v28c0 17.397 14.103 31.5 31.5 31.5h444c20.126 0 36.5 16.374 36.5 36.5V321H128V196.5c0-20.126 16.374-36.5 36.5-36.5z m695 705h-695c-20.126 0-36.5-16.374-36.5-36.5V384h768v444.5c0 20.126-16.374 36.5-36.5 36.5z',
fill: '#070102'
fill: 'currentColor'
})
]
)
])
}
},
setting: {
'app-applicaiton': {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M512 328c-100.8 0-184 83.2-184 184S411.2 696 512 696 696 612.8 696 512 612.8 328 512 328z m0 320c-75.2 0-136-60.8-136-136s60.8-136 136-136 136 60.8 136 136-60.8 136-136 136z',
fill: '#070102'
}),
h('path', {
d: 'M857.6 572.8c-20.8-12.8-33.6-35.2-33.6-60.8s12.8-46.4 33.6-60.8c14.4-9.6 20.8-27.2 16-44.8-8-27.2-19.2-52.8-32-76.8-8-14.4-25.6-24-43.2-19.2-24 4.8-48-1.6-65.6-19.2-17.6-17.6-24-41.6-19.2-65.6 3.2-16-4.8-33.6-19.2-43.2-24-14.4-51.2-24-76.8-32-16-4.8-35.2 1.6-44.8 16-12.8 20.8-35.2 33.6-60.8 33.6s-46.4-12.8-60.8-33.6c-9.6-14.4-27.2-20.8-44.8-16-27.2 8-52.8 19.2-76.8 32-14.4 8-24 25.6-19.2 43.2 4.8 24-1.6 49.6-19.2 65.6-17.6 17.6-41.6 24-65.6 19.2-16-3.2-33.6 4.8-43.2 19.2-14.4 24-24 51.2-32 76.8-4.8 16 1.6 35.2 16 44.8 20.8 12.8 33.6 35.2 33.6 60.8s-12.8 46.4-33.6 60.8c-14.4 9.6-20.8 27.2-16 44.8 8 27.2 19.2 52.8 32 76.8 8 14.4 25.6 22.4 43.2 19.2 24-4.8 49.6 1.6 65.6 19.2 17.6 17.6 24 41.6 19.2 65.6-3.2 16 4.8 33.6 19.2 43.2 24 14.4 51.2 24 76.8 32 16 4.8 35.2-1.6 44.8-16 12.8-20.8 35.2-33.6 60.8-33.6s46.4 12.8 60.8 33.6c8 11.2 20.8 17.6 33.6 17.6 3.2 0 8 0 11.2-1.6 27.2-8 52.8-19.2 76.8-32 14.4-8 24-25.6 19.2-43.2-4.8-24 1.6-49.6 19.2-65.6 17.6-17.6 41.6-24 65.6-19.2 16 3.2 33.6-4.8 43.2-19.2 14.4-24 24-51.2 32-76.8 4.8-17.6-1.6-35.2-16-44.8z m-56 92.8c-38.4-6.4-76.8 6.4-104 33.6-27.2 27.2-40 65.6-33.6 104-17.6 9.6-36.8 17.6-56 24-22.4-30.4-57.6-49.6-97.6-49.6-38.4 0-73.6 17.6-97.6 49.6-19.2-6.4-38.4-14.4-56-24 6.4-38.4-6.4-76.8-33.6-104-27.2-27.2-65.6-40-104-33.6-9.6-17.6-17.6-36.8-24-56 30.4-22.4 49.6-57.6 49.6-97.6 0-38.4-17.6-73.6-49.6-97.6 6.4-19.2 14.4-38.4 24-56 38.4 6.4 76.8-6.4 104-33.6 27.2-27.2 40-65.6 33.6-104 17.6-9.6 36.8-17.6 56-24 22.4 30.4 57.6 49.6 97.6 49.6 38.4 0 73.6-17.6 97.6-49.6 19.2 6.4 38.4 14.4 56 24-6.4 38.4 6.4 76.8 33.6 104 27.2 27.2 65.6 40 104 33.6 9.6 17.6 17.6 36.8 24 56-30.4 22.4-49.6 57.6-49.6 97.6 0 38.4 17.6 73.6 49.6 97.6-6.4 19.2-14.4 38.4-24 56z',
fill: '#070102'
d: 'M951.901 244.015l-413.3-238.57a33.606 33.606 0 0 0-33.909 0L91.3 244.016c-10.426 6.12-16.99 17.221-16.99 29.346v477.184c0 12.149 6.447 23.343 16.99 29.37l413.3 238.662c5.213 2.933 11.101 4.515 16.99 4.515 5.794 0 11.775-1.582 16.988-4.515l413.3-238.661c10.427-6.121 16.99-17.222 16.99-29.37V273.36a33.908 33.908 0 0 0-16.966-29.346zM892.23 726.016l-370.618 213.97-370.642-213.97v-427.87L521.588 84.178l370.642 213.97v427.869z m8.797 5.073M285.207 348.393a34.095 34.095 0 0 0-46.336 12.567 33.908 33.908 0 0 0 12.474 46.36l235.94 136.215v269.498a33.745 33.745 0 0 0 33.884 33.885 33.745 33.745 0 0 0 33.886-33.885V543.977L791.9 407.227a34.025 34.025 0 0 0 12.451-46.36 34.025 34.025 0 0 0-46.336-12.474l-236.404 136.54-236.405-136.54z m0 0',
fill: 'currentColor'
})
]
)
])
}
},
password: {
'app-exit': {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M807.28626 393.9647l-59.057047 0 0-78.729086c0-130.193201-105.96438-236.099253-236.229213-236.099253S275.770787 185.042413 275.770787 315.235614l0 78.729086-59.057047 0c-32.616862 0-59.057047 26.425859-59.057047 59.025325L157.656693 885.838314c0 32.598442 26.441209 59.025325 59.057047 59.025325l590.57252 0c32.616862 0 59.057047-26.425859 59.057047-59.025325L866.343307 452.989001C866.343307 420.390559 839.903122 393.9647 807.28626 393.9647zM334.827835 315.235614c0-97.644901 79.473029-177.074951 177.172165-177.074951s177.172165 79.43005 177.172165 177.074951l0 78.729086L334.827835 393.9647 334.827835 315.235614zM807.28626 885.838314 216.71374 885.838314 216.71374 452.989001l590.57252 0L807.28626 885.838314z'
}),
h('path', {
d: 'M512 777.635963c16.302291 0 29.528524-13.219069 29.528524-29.512151L541.528524 590.723969c0-16.293081-13.226233-29.512151-29.528524-29.512151s-29.528524 13.219069-29.528524 29.512151l0 157.399843C482.471476 764.416893 495.697709 777.635963 512 777.635963z'
d: 'M874.666667 855.744a19.093333 19.093333 0 0 1-19.136 18.922667H168.469333A19.2 19.2 0 0 1 149.333333 855.530667V168.469333A19.2 19.2 0 0 1 168.469333 149.333333h687.061334c10.581333 0 19.136 8.533333 19.136 18.922667V320h42.666666V168.256A61.717333 61.717333 0 0 0 855.530667 106.666667H168.469333A61.866667 61.866667 0 0 0 106.666667 168.469333v687.061334A61.866667 61.866667 0 0 0 168.469333 917.333333h687.061334A61.76 61.76 0 0 0 917.333333 855.744V704h-42.666666v151.744zM851.84 533.333333l-131.797333 131.754667a21.141333 21.141333 0 0 0 0.213333 29.973333 21.141333 21.141333 0 0 0 29.973333 0.192l165.589334-165.589333a20.821333 20.821333 0 0 0 6.122666-14.976 21.44 21.44 0 0 0-6.314666-14.997333l-168.533334-168.533334a21.141333 21.141333 0 0 0-29.952-0.213333 21.141333 21.141333 0 0 0 0.213334 29.973333L847.296 490.666667H469.333333v42.666666h382.506667z',
fill: 'currentColor'
})
]
)
])
}
},
exit: {
'app-team': {
iconReader: () => {
return h('el-icon', { style: 'display:flex' }, [
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
style: 'height:14px;width:14px',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M874.666667 855.744a19.093333 19.093333 0 0 1-19.136 18.922667H168.469333A19.2 19.2 0 0 1 149.333333 855.530667V168.469333A19.2 19.2 0 0 1 168.469333 149.333333h687.061334c10.581333 0 19.136 8.533333 19.136 18.922667V320h42.666666V168.256A61.717333 61.717333 0 0 0 855.530667 106.666667H168.469333A61.866667 61.866667 0 0 0 106.666667 168.469333v687.061334A61.866667 61.866667 0 0 0 168.469333 917.333333h687.061334A61.76 61.76 0 0 0 917.333333 855.744V704h-42.666666v151.744zM851.84 533.333333l-131.797333 131.754667a21.141333 21.141333 0 0 0 0.213333 29.973333 21.141333 21.141333 0 0 0 29.973333 0.192l165.589334-165.589333a20.821333 20.821333 0 0 0 6.122666-14.976 21.44 21.44 0 0 0-6.314666-14.997333l-168.533334-168.533334a21.141333 21.141333 0 0 0-29.952-0.213333 21.141333 21.141333 0 0 0 0.213334 29.973333L847.296 490.666667H469.333333v42.666666h382.506667z'
d: 'M 824.2 699.9 c -25.4 -25.4 -54.7 -45.7 -86.4 -60.4 C 783.1 602.8 812 546.8 812 484 c 0 -110.8 -92.4 -201.7 -203.2 -200 c -109.1 1.7 -197 90.6 -197 200 c 0 62.8 29 118.8 74.2 155.5 c -31.7 14.7 -60.9 34.9 -86.4 60.4 C 345 754.6 314 826.8 312 903.8 c -0.1 4.5 3.5 8.2 8 8.2 h 56 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 C 493.8 707.7 551.1 684 612 684 c 60.9 0 118.2 23.7 161.3 66.8 C 814.5 792 838 846.3 840 904.3 c 0.1 4.3 3.7 7.7 8 7.7 h 56 c 4.5 0 8.1 -3.7 8 -8.2 c -2 -77 -33 -149.2 -87.8 -203.9 Z M 612 612 c -34.2 0 -66.4 -13.3 -90.5 -37.5 c -24.5 -24.5 -37.9 -57.1 -37.5 -91.8 c 0.3 -32.8 13.4 -64.5 36.3 -88 c 24 -24.6 56.1 -38.3 90.4 -38.7 c 33.9 -0.3 66.8 12.9 91 36.6 c 24.8 24.3 38.4 56.8 38.4 91.4 c 0 34.2 -13.3 66.3 -37.5 90.5 c -24.2 24.2 -56.4 37.5 -90.6 37.5 Z M 361.5 510.4 c -0.9 -8.7 -1.4 -17.5 -1.4 -26.4 c 0 -15.9 1.5 -31.4 4.3 -46.5 c 0.7 -3.6 -1.2 -7.3 -4.5 -8.8 c -13.6 -6.1 -26.1 -14.5 -36.9 -25.1 c -25.8 -25.2 -39.7 -59.3 -38.7 -95.4 c 0.9 -32.1 13.8 -62.6 36.3 -85.6 c 24.7 -25.3 57.9 -39.1 93.2 -38.7 c 31.9 0.3 62.7 12.6 86 34.4 c 7.9 7.4 14.7 15.6 20.4 24.4 c 2 3.1 5.9 4.4 9.3 3.2 c 17.6 -6.1 36.2 -10.4 55.3 -12.4 c 5.6 -0.6 8.8 -6.6 6.3 -11.6 c -32.5 -64.3 -98.9 -108.7 -175.7 -109.9 c -110.9 -1.7 -203.3 89.2 -203.3 199.9 c 0 62.8 28.9 118.8 74.2 155.5 c -31.8 14.7 -61.1 35 -86.5 60.4 c -54.8 54.7 -85.8 126.9 -87.8 204 c -0.1 4.5 3.5 8.2 8 8.2 h 56.1 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 c 29.4 -29.4 65.4 -49.8 104.7 -59.7 c 3.9 -1 6.5 -4.7 6 -8.7 Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-add-users': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M 892 772 h -80 v -80 c 0 -4.4 -3.6 -8 -8 -8 h -48 c -4.4 0 -8 3.6 -8 8 v 80 h -80 c -4.4 0 -8 3.6 -8 8 v 48 c 0 4.4 3.6 8 8 8 h 80 v 80 c 0 4.4 3.6 8 8 8 h 48 c 4.4 0 8 -3.6 8 -8 v -80 h 80 c 4.4 0 8 -3.6 8 -8 v -48 c 0 -4.4 -3.6 -8 -8 -8 Z M 373.5 498.4 c -0.9 -8.7 -1.4 -17.5 -1.4 -26.4 c 0 -15.9 1.5 -31.4 4.3 -46.5 c 0.7 -3.6 -1.2 -7.3 -4.5 -8.8 c -13.6 -6.1 -26.1 -14.5 -36.9 -25.1 c -25.8 -25.2 -39.7 -59.3 -38.7 -95.4 c 0.9 -32.1 13.8 -62.6 36.3 -85.6 c 24.7 -25.3 57.9 -39.1 93.2 -38.7 c 31.9 0.3 62.7 12.6 86 34.4 c 7.9 7.4 14.7 15.6 20.4 24.4 c 2 3.1 5.9 4.4 9.3 3.2 c 17.6 -6.1 36.2 -10.4 55.3 -12.4 c 5.6 -0.6 8.8 -6.6 6.3 -11.6 c -32.5 -64.3 -98.9 -108.7 -175.7 -109.9 c -110.8 -1.7 -203.2 89.2 -203.2 200 c 0 62.8 28.9 118.8 74.2 155.5 c -31.8 14.7 -61.1 35 -86.5 60.4 c -54.8 54.7 -85.8 126.9 -87.8 204 c -0.1 4.5 3.5 8.2 8 8.2 h 56.1 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 c 29.4 -29.4 65.4 -49.8 104.7 -59.7 c 3.8 -1.1 6.4 -4.8 5.9 -8.8 Z M 824 472 c 0 -109.4 -87.9 -198.3 -196.9 -200 C 516.3 270.3 424 361.2 424 472 c 0 62.8 29 118.8 74.2 155.5 c -31.7 14.7 -60.9 34.9 -86.4 60.4 C 357 742.6 326 814.8 324 891.8 c -0.1 4.5 3.5 8.2 8 8.2 h 56 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 C 505.8 695.7 563 672 624 672 c 110.4 0 200 -89.5 200 -200 Z m -109.5 90.5 C 690.3 586.7 658.2 600 624 600 s -66.3 -13.3 -90.5 -37.5 C 509 538 495.7 505.4 496 470.7 c 0.3 -32.8 13.4 -64.5 36.3 -88 c 24 -24.6 56.1 -38.3 90.4 -38.7 c 33.9 -0.3 66.8 12.9 91 36.6 c 24.8 24.3 38.4 56.8 38.4 91.4 c -0.1 34.2 -13.4 66.3 -37.6 90.5 Z',
fill: 'currentColor'
})
]
)

View File

@ -0,0 +1,16 @@
import { type App } from 'vue'
import AppIcon from './icons/AppIcon.vue'
import LoginLayout from './login-layout/index.vue'
import LoginContainer from './login-container/index.vue'
import LayoutContent from './content-container/LayoutContent.vue'
import TagsInput from './tags-input/index.vue'
export default {
install(app: App) {
app.component(AppIcon.name, AppIcon)
app.component(LoginLayout.name, LoginLayout)
app.component(LoginContainer.name, LoginContainer)
app.component(LayoutContent.name, LayoutContent)
app.component(TagsInput.name, TagsInput)
}
}

View File

@ -1,66 +0,0 @@
<template>
<div class="login-warp">
<div class="login-container">
<el-row class="container">
<el-col :span="14" class="left-container">
<div class="login-image"></div>
</el-col>
<el-col :span="10" class="right-container">
<slot></slot>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scope>
.login-warp {
display: flex;
align-items: center;
justify-content: center;
align-content: center;
height: 100%;
width: 100%;
.login-container {
width: 100%;
height: 100%;
.container {
height: 100%;
width: 100%;
.right-container {
display: flex;
margin-top: 20vh;
justify-content: center;
width: 100%;
}
.left-container {
.login-image {
background-image: url('@/assets/login.png');
background-size: 100% 100%;
width: 100%;
height: 100%;
}
}
}
}
}
</style>

View File

@ -1,155 +0,0 @@
<template >
<el-dialog v-model="resetPasswordDialog" title="修改密码">
<el-form class="reset-password-form" ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="rules">
<el-form-item prop="password">
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.password"
placeholder="请输入密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="re_password">
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.re_password"
placeholder="请输入确认密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input size="large" class="input-item" :disabled="true" v-bind:modelValue="userStore.userInfo?.email"
@change="() => { }" placeholder="请输入邮箱">
<template #prepend>
<el-button :icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input size="large" class="code-input" v-model="resetPasswordForm.code" placeholder="请输入验证码">
<template #prepend>
<el-button :icon="Key" />
</template>
</el-input>
<el-button size="large" class="send-email-button" @click="sendEmail" :loading="loading">获取验证码</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="resetPassword">
修改密码
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { ResetCurrentUserPasswordRequest } from "@/api/user/type";
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from "element-plus"
import UserApi from "@/api/user"
import { useUserStore } from '@/stores/user';
import { Lock, UserFilled, Key } from '@element-plus/icons-vue'
import { useRouter } from "vue-router"
const router = useRouter();
const userStore = useUserStore()
const resetPasswordDialog = ref<boolean>(false);
const resetPasswordForm = ref<ResetCurrentUserPasswordRequest>({
code: "",
password: "",
re_password: ""
});
const resetPasswordFormRef = ref<FormInstance>();
const loading = ref<boolean>(false);
const rules = ref<FormRules<ResetCurrentUserPasswordRequest>>({
code: [
{ required: true, message: '请输入验证码' }
],
password: [
{
required: true,
message: "请输入密码",
trigger: "blur",
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
],
re_password: [{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error('密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}]
})
/**
* 发送验证码
*/
const sendEmail = () => {
UserApi.sendEmailToCurrent(loading)
.then(() => {
ElMessage.success("发送验证码成功")
})
}
const open = () => {
resetPasswordForm.value = {
code: "",
password: "",
re_password: ""
}
resetPasswordDialog.value = true
}
const resetPassword = () => {
resetPasswordFormRef.value?.validate().then(() => {
return UserApi.resetCurrentUserPassword(resetPasswordForm.value)
}).then(() => {
return userStore.logout()
}).then(() => {
router.push({ name: 'login' })
})
}
const close = () => { resetPasswordDialog.value = false }
defineExpose({ open, close })
</script>
<style lang="scss" scope>
.code-input {
width: 250px;
}
.send-email-button {
margin-left: 12px;
width: 158px;
}
</style>

View File

@ -1,50 +0,0 @@
<template >
<el-dropdown trigger="click" size="small" type="primary">
<el-avatar> {{ firstUserName }} </el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openResetPassword">
<AppIcon iconName="password"></AppIcon><span style="margin-left:5px">修改密码</span>
</el-dropdown-item>
<el-dropdown-item @click="logout">
<AppIcon iconName="exit"></AppIcon><span style="margin-left:5px">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<ResetPassword ref="resetPasswordRef"></ResetPassword>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useUserStore } from '@/stores/user';
import { useRouter } from "vue-router";
import AppIcon from "@/components/icons/AppIcon.vue"
import ResetPassword from "@/components/layout/top-bar/components/avatar/ResetPasssword.vue"
const userStore = useUserStore()
const router = useRouter()
const firstUserName = computed(() => {
return userStore.userInfo?.username?.substring(0, 1)
})
const resetPasswordRef = ref<InstanceType<typeof ResetPassword>>();
const openResetPassword = () => {
resetPasswordRef.value?.open()
}
const logout = () => {
userStore.logout().then(() => {
router.push({ name: "login" })
})
}
</script>
<style lang="scss" scoped>
.el-avatar {
--el-avatar-size: 30px;
--el-avatar-bg-color: var(--app-base-action-text-color);
cursor: pointer;
}
.el-dropdown-menu--small {
padding: 10px 0;
}
</style>

View File

@ -1,40 +0,0 @@
<template>
<div class="menu-item-container" :class="isActive ? 'active' : ''" @click="router.push({ name: menu.name })">
<div class="icon">
<AppIcon :iconName="menu.meta ? menu.meta.icon as string : '404'"></AppIcon>
</div>
<div class="title">{{ menu.meta?.title }} </div>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'
import { computed } from "vue";
import AppIcon from "@/components/icons/AppIcon.vue"
const router = useRouter();
const route = useRoute();
const props = defineProps<{
menu: RouteRecordRaw
}>()
const isActive = computed(() => {
return route.name == props.menu.name && route.path == props.menu.path
})
</script>
<style lang="scss" scoped>
.menu-item-container {
padding: 0 20px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
height: 100%;
}
.active {
background-color: var(--app-base-text-hover-bg-color);
border-bottom: 3px solid var(--app-base-text-hover-color);
height: calc(100% - 3px);
}
</style>

View File

@ -1,22 +0,0 @@
<template >
<div class="top-menu-container">
<MenuItem :menu="menu" v-hasPermission="menu.meta?.permission" v-for="(menu, index) in topMenuList" :key="index">
</MenuItem>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { getChildRouteListByPathAndName } from "@/router/index"
import MenuItem from "@/components/layout/top-bar/components/top-menu/MenuItem.vue"
const topMenuList = computed(() => {
return getChildRouteListByPathAndName("/", "home")
})
</script>
<style lang="scss" scope>
.top-menu-container {
display: flex;
align-items: center;
height: 100%;
}
</style>

View File

@ -1,73 +0,0 @@
<template>
<div class="top-bar-container">
<div class="app-title-container">
<div class="app-title-icon"></div>
<div class="app-title-text">智能客服</div>
<div class="line"></div>
</div>
<div class="app-top-menu-container">
<TopMenu></TopMenu>
</div>
<div class="flex-auto"></div>
<div class="avatar">
<Avatar></Avatar>
</div>
</div>
</template>
<script setup lang="ts">
import TopMenu from "@/components/layout/top-bar/components/top-menu/index.vue"
import Avatar from "@/components/layout/top-bar/components/avatar/index.vue"
</script>
<style lang="scss">
.top-bar-container {
border-bottom: 1px solid rgba(229, 229, 229, 1);
height: calc(100% - 1px);
background-color: var(--app-header-background-color, #fff);
width: 100vw;
display: flex;
.flex-auto {
flex: 1 1 auto;
}
.avatar {
height: 100%;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
}
.app-title-container {
width: 200px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.app-title-icon {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
.app-title-text {
color: var(--app-base-action-text-color);
font-size: 28px;
font-weight: 600;
align-items: center;
}
.line {
height: 60%;
width: 1px;
margin-left: 20px;
background-color: rgba(229, 229, 229, 1);
}
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="login-form-container">
<div class="login-title">
<div class="title flex-center">
<div class="logo"></div>
<div>{{ title || defaultTitle }}</div>
</div>
<div class="sub-title" v-if="subTitle">{{ subTitle }}</div>
</div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
const defaultTitle = import.meta.env.VITE_APP_TITLE
defineOptions({ name: 'LoginContainer' })
defineProps({
title: String,
subTitle: String
})
</script>
<style lang="scss" scope>
.login-form-container {
width: 420px;
.login-title {
margin-bottom: 30px;
.title {
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
.logo {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
}
.sub-title {
text-align: center;
color: #101010;
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="login-warp flex-center">
<div class="login-container w-full h-full">
<el-row class="container w-full h-full">
<el-col
:xs="8"
:sm="6"
:md="14"
:lg="14"
:xl="14"
class="left-container"
v-if="screenWidth && screenWidth >= 990"
>
<div class="login-image"></div>
</el-col>
<el-col :xs="24" :sm="24" :md="10" :lg="10" :xl="10" class="right-container flex-center">
<slot></slot>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { Ref } from 'vue'
defineOptions({ name: 'LoginLayout' })
const screenWidth: Ref<number | null> = ref(null)
onMounted(() => {
screenWidth.value = document.body.clientWidth
window.onresize = () => {
return (() => {
screenWidth.value = document.body.clientWidth
})()
}
})
</script>
<style lang="scss" scope>
.login-warp {
height: 100vh;
.login-image {
background: url(@/assets/login.png) no-repeat;
background-size: 100% 100%;
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<!-- 外层div -->
<div ref="InputTag" class="tags-input">
<div class="tags-container" v-if="tagsList.length">
<!-- 标签 -->
<el-tag
v-for="(item, index) in tagsList"
:key="index"
@close="removeTag(item)"
closable
class="mr-10"
>{{ item }}
</el-tag>
</div>
<!-- 输入框 -->
<el-input
:validate-event="false"
v-model="currentval"
:placeholder="tagsList.length == 0 ? placeholder : ''"
@keydown.enter="addTags"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
defineOptions({ name: 'TagsInput' })
const props = defineProps({
tags: {
//
type: Array<String>,
default: () => []
},
tag: {
//
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入'
},
limit: {
//
type: Number,
default: -1
},
reg: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:tags', 'update:tag'])
const currentval = ref('')
const tagsList = ref<String[]>([])
watch([tagsList, currentval], (val) => {
if (val[0]?.length > 0) {
emit('update:tags', val[0])
} else if (val[1]) {
emit('update:tag', val[1])
}
})
function addTags() {
const val = currentval.value.trim()
if (val) {
tagsList.value.push(val)
}
currentval.value = ''
}
function removeTag(tag: String) {
tagsList.value.splice(tagsList.value.indexOf(tag), 1)
}
</script>
<style lang="scss" scoped>
.tags-input {
width: 100%;
min-height: 70px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
:deep(.el-input__wrapper) {
background: none !important;
box-shadow: none !important;
border-radius: 0 !important;
resize: none;
}
.tags-container {
padding: 0 6px;
}
}
</style>

View File

@ -1,10 +1,10 @@
import type { App } from 'vue'
import { hasPermission } from '@/common/permission'
import { hasPermission } from '@/utils/permission'
const display = async (el: any, binding: any) => {
const has = hasPermission(
binding.value.permission ? binding.value.permission : binding.value,
binding.value.compare ? binding.value.compare : 'OR'
binding.value?.permission || binding.value,
binding.value?.compare || 'OR'
)
if (!has) {
el.style.display = 'none'

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { TopBar, AppMain } from '../components'
</script>
<template>
<div class="app-layout">
<div class="app-header">
<TopBar />
</div>
<div class="app-main">
<AppMain />
</div>
</div>
</template>
<style lang="scss" scoped>
.app-layout {
background-color: var(--app-layout-bg-color);
}
.app-main {
height: calc(100vh - var(--app-header-height));
padding: 0 !important;
}
.app-header {
background-color: var(--app-header-bg-color);
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<router-view v-slot="{ Component }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup lang="ts">
import { ref, onBeforeUpdate } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const cachedViews: any = ref([])
onBeforeUpdate(() => {
let isCached = route.meta?.cache
let name = route.name
if (isCached && name && !cachedViews.value.includes(name)) {
cachedViews.value.push(name)
}
})
</script>

View File

@ -0,0 +1,3 @@
export { default as Sidebar } from './sidebar/index.vue'
export { default as AppMain } from './app-main/index.vue'
export { default as TopBar } from './top-bar/index.vue'

View File

@ -0,0 +1,32 @@
<template>
<div v-if="!menu.meta || !menu.meta.hidden" class="sidebar-item">
<el-menu-item ref="subMenu" :index="menu.path" popper-class="sidebar-popper">
<template #title>
<AppIcon v-if="menu.meta && menu.meta.icon" :iconName="menu.meta.icon" />
<span v-if="menu.meta && menu.meta.title">{{ menu.meta.title }}</span>
</template>
</el-menu-item>
</div>
</template>
<script setup lang="ts">
import { type RouteRecordRaw } from 'vue-router'
defineProps<{
menu: RouteRecordRaw
}>()
</script>
<style scoped lang="scss">
.sidebar-item {
.el-menu-item {
padding-left: 30px !important;
font-weight: 500;
}
.el-menu-item.is-active {
color: var(--el-menu-active-color);
background: var(--el-color-primary-light-9);
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="sidebar">
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu :default-active="activeMenu">
<sidebar-item
:menu="menu"
v-hasPermission="menu.meta?.permission"
v-for="(menu, index) in subMenuList"
:key="index"
>
</sidebar-item>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { getChildRouteListByPathAndName } from '@/router/index'
import SidebarItem from './SidebarItem.vue'
const route = useRoute()
const subMenuList = computed(() => {
return getChildRouteListByPathAndName(route.path, route.name)
})
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
</script>
<style lang="scss">
.sidebar {
.el-menu {
height: 100%;
border: none;
}
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<el-dialog v-model="resetPasswordDialog" title="修改密码">
<el-form
class="reset-password-form"
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="rules"
>
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.password"
placeholder="请输入密码"
>
<template #prepend>
<el-button icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="re_password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.re_password"
placeholder="请输入确认密码"
>
<template #prepend>
<el-button icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input
size="large"
class="input-item"
:disabled="true"
v-bind:modelValue="user.userInfo?.email"
@change="() => {}"
placeholder="请输入邮箱"
>
<template #prepend>
<el-button icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<div class="flex-between w-full">
<el-input
size="large"
class="code-input"
v-model="resetPasswordForm.code"
placeholder="请输入验证码"
>
<template #prepend>
<el-button icon="Key" />
</template>
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
@click="sendEmail"
:loading="loading"
>获取验证码</el-button
>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="resetPassword"> 修改密码 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ResetCurrentUserPasswordRequest } from '@/api/type/user'
import type { FormInstance, FormRules } from 'element-plus'
import { MsgSuccess } from '@/utils/message'
import UserApi from '@/api/user'
import useStore from '@/stores'
import { useRouter } from 'vue-router'
const router = useRouter()
const { user } = useStore()
const resetPasswordDialog = ref<boolean>(false)
const resetPasswordForm = ref<ResetCurrentUserPasswordRequest>({
code: '',
password: '',
re_password: ''
})
const resetPasswordFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const rules = ref<FormRules<ResetCurrentUserPasswordRequest>>({
code: [{ required: true, message: '请输入验证码' }],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
}
],
re_password: [
{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error('密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
/**
* 发送验证码
*/
const sendEmail = () => {
UserApi.sendEmailToCurrent(loading).then(() => {
MsgSuccess('发送验证码成功')
})
}
const open = () => {
resetPasswordForm.value = {
code: '',
password: '',
re_password: ''
}
resetPasswordDialog.value = true
}
const resetPassword = () => {
resetPasswordFormRef.value
?.validate()
.then(() => {
return UserApi.resetCurrentUserPassword(resetPasswordForm.value)
})
.then(() => {
return user.logout()
})
.then(() => {
router.push({ name: 'login' })
})
}
const close = () => {
resetPasswordDialog.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scope></style>

View File

@ -0,0 +1,39 @@
<template>
<el-dropdown trigger="click" type="primary">
<el-avatar :size="30"> {{ firstUserName }} </el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openResetPassword">
<AppIcon iconName="Lock"></AppIcon><span style="margin-left: 5px">修改密码</span>
</el-dropdown-item>
<el-dropdown-item @click="logout">
<AppIcon iconName="app-exit"></AppIcon><span style="margin-left: 5px">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<ResetPassword ref="resetPasswordRef"></ResetPassword>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import useStore from '@/stores';
import { useRouter } from 'vue-router'
import ResetPassword from './ResetPasssword.vue'
const { user } = useStore();
const router = useRouter()
const firstUserName = computed(() => {
return user.userInfo?.username?.substring(0, 1)
})
const resetPasswordRef = ref<InstanceType<typeof ResetPassword>>()
const openResetPassword = () => {
resetPasswordRef.value?.open()
}
const logout = () => {
user.logout().then(() => {
router.push({ name: 'login' })
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,46 @@
<template>
<div class="top-bar-container flex-between border-b">
<div class="flex-center h-full">
<div class="app-title-container flex-center">
<div class="app-title-icon"></div>
<div class="app-title-text ml-10">{{ defaultTitle }}</div>
</div>
<el-divider direction="vertical" class="line" />
<TopMenu></TopMenu>
</div>
<div class="avatar">
<Avatar></Avatar>
</div>
</div>
</template>
<script setup lang="ts">
import TopMenu from './top-menu/index.vue'
import Avatar from './avatar/index.vue'
const defaultTitle = import.meta.env.VITE_APP_TITLE
</script>
<style lang="scss">
.top-bar-container {
height: var(--app-header-height);
box-sizing: border-box;
padding: var(--app-header-padding);
.app-title-container {
margin-right: 20px;
.app-title-icon {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 40px;
height: 40px;
}
.app-title-text {
color: var(--el-color-primary);
font-size: 20px;
font-weight: 600;
}
}
.line {
height: 2em;
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div
class="menu-item-container flex-center h-full"
:class="isActive ? 'active' : ''"
@click="router.push({ name: menu.name })"
>
<div class="icon">
<AppIcon :iconName="menu.meta ? (menu.meta.icon as string) : '404'" />
</div>
<div class="title">{{ menu.meta?.title }}</div>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'
import { computed } from 'vue'
const router = useRouter()
const route = useRoute()
const props = defineProps<{
menu: RouteRecordRaw
}>()
const isActive = computed(() => {
return route.name == props.menu.name && route.path == props.menu.path
})
</script>
<style lang="scss" scoped>
.menu-item-container {
padding: 0 20px;
cursor: pointer;
.icon {
font-size: 15px;
margin-right: 5px;
margin-top: 2px;
}
&:hover {
color: var(--el-color-primary);
}
}
.active {
font-weight: 600;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-bottom: 3px solid var(--el-color-primary);
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="top-menu-container flex h-full">
<MenuItem
:menu="menu"
v-hasPermission="menu.meta?.permission"
v-for="(menu, index) in topMenuList"
:key="index"
>
</MenuItem>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getChildRouteListByPathAndName } from '@/router/index'
import MenuItem from './MenuItem.vue'
const topMenuList = computed(() => {
return getChildRouteListByPathAndName('/', 'home')
})
</script>
<style lang="scss" scope>
.top-menu-container {
align-items: center;
margin-bottom: -1px;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="main-layout h-full flex">
<div class="sidebar-container border-r"><Sidebar /></div>
<div class="view-container">
<AppMain />
</div>
</div>
</template>
<script setup lang="ts">
import { Sidebar, AppMain } from '../components'
</script>
<style lang="scss">
.sidebar-container {
box-sizing: border-box;
transition: width 0.28s;
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background-color: var(--sidebar-bg-color);
}
.view-container {
width: 100%;
padding: var(--app-view-padding);
}
</style>

View File

@ -1,25 +1,28 @@
import 'nprogress/nprogress.css'
import '@/styles/index.scss'
import ElementPlus from 'element-plus'
import * as ElementPlusIcons from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import { createApp } from 'vue'
import { store } from '@/stores'
import theme from '@/theme'
import directives from '@/directives'
import App from './App.vue'
import router from '@/router'
import Components from '@/components'
const app = createApp(App)
app.use(store)
app.use(directives)
const ElementPlusIconsVue: object = ElementPlusIcons
// 将elementIcon放到全局
app.config.globalProperties.$antIcons = ElementPlusIconsVue
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIcons)) {
app.component(key, component)
}
app.use(ElementPlus, {
locale: zhCn
})
app.use(theme)
app.use(router)
app.use(Components)
app.mount('#app')

View File

@ -1,10 +1,9 @@
import axios, { type AxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
import { MsgError } from '@/utils/message'
import type { NProgress } from 'nprogress'
import type { Ref } from 'vue'
import type { Result } from '@/request/Result'
import { store } from '@/stores/index'
import { useUserStore } from '@/stores/user'
import useStore from '@/stores'
import router from '@/router'
import { ref, type WritableComputedRef } from 'vue'
@ -24,10 +23,10 @@ instance.interceptors.request.use(
if (config.headers === undefined) {
config.headers = {}
}
const userStore = useUserStore(store)
const token = userStore.getToken()
const { user } = useStore()
const token = user.getToken()
if (token) {
config.headers['AUTHORIZATION'] = token
config.headers['AUTHORIZATION'] = `${token}`
}
return config
},
@ -41,7 +40,7 @@ instance.interceptors.response.use(
(response: any) => {
if (response.data) {
if (response.data.code !== 200 && !(response.data instanceof Blob)) {
ElMessage.error(response.data.message)
MsgError(response.data.message)
}
}
if (response.headers['content-type'] === 'application/octet-stream') {
@ -51,7 +50,7 @@ instance.interceptors.response.use(
},
(err: any) => {
if (err.code === 'ECONNABORTED') {
ElMessage.error(err.message)
MsgError(err.message)
console.error(err)
}
if (err.response?.status === 401) {
@ -59,7 +58,7 @@ instance.interceptors.response.use(
}
if (err.response?.status === 403) {
ElMessage.error(
MsgError(
err.response.data && err.response.data.message ? err.response.data.message : '没有权限访问'
)
}
@ -130,10 +129,10 @@ export const get: (
*/
export const post: (
url: string,
params?: unknown,
data?: unknown,
params?: unknown,
loading?: NProgress | Ref<boolean>
) => Promise<Result<any> | any> = (url, params, data, loading) => {
) => Promise<Result<any> | any> = (url, data, params, loading) => {
return promise(request({ url: url, method: 'post', data, params }), loading)
}
@ -151,7 +150,7 @@ export const put: (
data?: unknown,
loading?: NProgress | Ref<boolean>
) => Promise<Result<any>> = (url, params, data, loading) => {
return promise(request({ url: url, method: 'put', data, params }), loading)
return promise(request({ url: url, method: 'put', params, data }), loading)
}
/**
@ -167,7 +166,7 @@ export const del: (
data?: unknown,
loading?: NProgress | Ref<boolean>
) => Promise<Result<any>> = (url, params, data, loading) => {
return promise(request({ url: url, method: 'delete', data, params }), loading)
return promise(request({ url: url, method: 'delete', params, data }), loading)
}
export const exportExcel: (

View File

@ -1,60 +0,0 @@
import type { RouteRecordRaw } from 'vue-router'
import { Role } from '@/common/permission/type'
export const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('@/views/home/index.vue'),
children: [
{
path: '/first',
name: 'first',
meta: { icon: 'app', title: '首页' },
component: () => import('@/views/first/index.vue')
},
{
path: '/app',
name: 'app',
meta: { icon: 'app', title: '应用', permission: 'APPLICATION:READ' },
component: () => import('@/views/app/index.vue')
},
{
path: '/dataset',
name: 'dataset',
meta: { icon: 'dataset', title: '数据集', permission: 'DATASET:READ' },
component: () => import('@/views/dataset/index.vue')
},
{
path: '/setting',
name: 'setting',
meta: { icon: 'setting', title: '数据设置', permission: 'SETTING:READ' },
component: () => import('@/views/setting/index.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue')
},
{
path: '/register',
name: 'register',
component: () => import('@/views/register/index.vue')
},
{
path: '/forgot_password',
name: 'forgot_password',
component: () => import('@/views/forgot-password/index.vue')
},
{
path: '/reset_password/:code/:email',
name: 'reset_password',
component: () => import('@/views/reset-password/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('@/views/404/index.vue')
}
]

View File

@ -1,21 +1,19 @@
import { hasPermission } from '@/common/permission/index'
import { hasPermission } from '@/utils/permission/index'
import {
createRouter,
createWebHistory,
type NavigationGuardNext,
type RouteLocationNormalized,
type RouteRecordRaw
type RouteRecordRaw,
type RouteRecordName
} from 'vue-router'
import { useUserStore } from '@/stores/user'
import { store } from '@/stores'
import { routes } from '@/router/data'
import useStore from '@/stores';
import { routes } from '@/router/routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes
})
// 解决刷新获取用户信息问题
let userStore: any = null
// 路由前置拦截器
router.beforeEach(
@ -24,21 +22,19 @@ router.beforeEach(
next()
return
}
if (userStore === null) {
userStore = useUserStore(store)
}
const { user } = useStore();
const notAuthRouteNameList = ['register', 'login', 'forgot_password', 'reset_password']
if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) {
const token = userStore.getToken()
const token = user.getToken()
if (!token) {
next({
path: '/login'
})
return
}
if (!userStore.userInfo) {
await userStore.profile()
if (!user.userInfo) {
await user.profile()
}
}
// 判断是否有菜单权限
@ -51,14 +47,14 @@ router.beforeEach(
}
)
export const getChildRouteListByPathAndName = (path: string, name: string) => {
export const getChildRouteListByPathAndName = (path: string, name?: RouteRecordName | null | undefined) => {
return getChildRouteList(routes, path, name)
}
export const getChildRouteList: (
routeList: Array<RouteRecordRaw>,
path: string,
name: string
name?: RouteRecordName | null | undefined
) => Array<RouteRecordRaw> = (routeList, path, name) => {
for (let index = 0; index < routeList.length; index++) {
const route = routeList[index]

View File

@ -0,0 +1,8 @@
const applicationRouter = {
path: '/app',
name: 'app',
meta: { icon: 'app-applicaiton', title: '应用', permission: 'APPLICATION:READ' },
component: () => import('@/views/app/index.vue')
}
export default applicationRouter

View File

@ -0,0 +1,32 @@
import Layout from '@/layout/main-layout/index.vue'
const datasetRouter = {
path: '/dataset',
name: 'dataset',
meta: { icon: 'app-dataset', title: '数据集', permission: 'DATASET:READ' },
redirect: '/dataset',
children: [
{
path: '/dataset',
name: 'dataset',
component: () => import('@/views/dataset/index.vue')
},
{
path: '/dataset/doc',
name: 'DatasetDoc',
meta: { icon: 'House', title: '文档', activeMenu: '/dataset' },
component: Layout,
hidden: true,
redirect: '/dataset/doc',
children: [
{
path: '/dataset/doc',
name: 'DatasetDoc',
meta: { icon: 'House', title: '文档' },
component: () => import('@/views/dataset/DatasetDoc.vue')
}
]
}
]
}
export default datasetRouter

View File

@ -0,0 +1,18 @@
import Layout from '@/layout/main-layout/index.vue'
const settingRouter = {
path: '/setting',
name: 'setting',
meta: { icon: 'Setting', title: '系统设置', permission: 'SETTING:READ' },
redirect: '/setting',
component: Layout,
children: [
{
path: '/setting',
name: 'setting',
meta: { icon: 'app-team', title: '团队管理' },
component: () => import('@/views/setting/index.vue')
}
]
}
export default settingRouter

49
ui/src/router/routes.ts Normal file
View File

@ -0,0 +1,49 @@
import type { RouteRecordRaw } from 'vue-router'
import { Role } from '@/utils/permission/type'
const modules: any = import.meta.glob('./modules/*.ts', { eager: true })
const rolesRoutes: RouteRecordRaw[] = [...Object.keys(modules).map((key) => modules[key].default)]
export const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('@/layout/app-layout/index.vue'),
redirect: '/setting',
children: [
{
path: '/first',
name: 'first',
meta: { icon: 'House', title: '首页' },
component: () => import('@/views/first/index.vue')
},
...rolesRoutes
]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue')
},
{
path: '/register',
name: 'register',
component: () => import('@/views/login/register/index.vue')
},
{
path: '/forgot_password',
name: 'forgot_password',
component: () => import('@/views/login/forgot-password/index.vue')
},
{
path: '/reset_password/:code/:email',
name: 'reset_password',
component: () => import('@/views/login/reset-password/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('@/views/404/index.vue')
}
]

View File

@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@ -1,9 +1,10 @@
import type { App } from "vue";
import { createPinia } from "pinia";
const store = createPinia();
import { createPinia } from 'pinia'
const store = createPinia()
export { store }
import useUserStore from './modules/user'
export function setupStore(app: App<Element>) {
app.use(store);
}
const useStore = () => ({
user: useUserStore()
})
export { store };
export default useStore

View File

@ -0,0 +1,62 @@
import { defineStore } from 'pinia'
import type { User } from '@/api/type/user'
import UserApi from '@/api/user'
export interface appStateTypes {
userInfo: User | null
token: any
}
const useUserStore = defineStore({
id: 'user',
state: (): appStateTypes => ({
userInfo: null,
token: ''
}),
actions: {
getToken(): String | null {
if (this.token) {
return this.token
}
return localStorage.getItem('token')
},
getPermissions() {
if (this.userInfo) {
return this.userInfo?.permissions
} else {
return []
}
},
getRole() {
if (this.userInfo) {
return this.userInfo?.role
} else {
return ''
}
},
async profile() {
return UserApi.profile().then((ok) => {
this.userInfo = ok.data
})
},
async login(username: string, password: string) {
return UserApi.login({ username, password }).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()
})
},
async logout() {
return UserApi.logout().then(() => {
localStorage.removeItem('token')
return true
})
}
}
})
export default useUserStore

View File

@ -1,55 +0,0 @@
import { defineStore } from 'pinia'
import type { User } from '@/api/user/type'
import UserApi from '@/api/user'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const userInfo = ref<User>()
// 用户认证token
const token = ref<string>()
const getToken = () => {
if (token.value) {
return token.value
}
return localStorage.getItem('token')
}
const getPermissions = () => {
if (userInfo.value) {
return userInfo.value.permissions
} else {
return []
}
}
const getRole = () => {
if (userInfo.value) {
return userInfo.value.role
} else {
return ''
}
}
const profile = () => {
return UserApi.profile().then((ok) => {
userInfo.value = ok.data
})
}
const login = (username: string, password: string) => {
return UserApi.login({ username, password }).then((ok) => {
token.value = ok.data
localStorage.setItem('token', ok.data)
return profile()
})
}
const logout = () => {
return UserApi.logout().then(() => {
localStorage.removeItem('token')
return true
})
}
return { token, getToken, userInfo, profile, login, logout, getPermissions, getRole }
})

View File

@ -1,21 +1,26 @@
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
box-sizing: border-box;
}
body {
font-family: Helvetica, PingFang SC, Arial, sans-serif;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
'微软雅黑', Arial, sans-serif;
height: 100%;
margin: 0;
padding: 0;
}
#app {
height:100%;
height: 100%;
}
:focus {
@ -34,6 +39,16 @@ a:hover {
text-decoration: none;
}
div:focus {
outline: none;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
// 滚动条整体部分
::-webkit-scrollbar {
width: 6px; // 纵向滚动条宽度
@ -52,177 +67,75 @@ a:hover {
background-color: transparent;
}
// 创建表单
.create-catalog-container {
.w-full {
width: 100%;
}
.h-full {
height: 100%;
margin-top: -20px;
.padding-top-30{
padding-top:30px;
}
.padding-top-40{
padding-top:40px;
}
// 表单外套
.form-div{
text-align: center;
margin: 0 auto;
width: 80%;
min-width: 300px;
form{
.el-form-item {margin-bottom: 28px;}
label{
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #1F2329;
flex: none;
order: 0;
flex-grow: 0;
}
}
}
// 删除按钮样式
.delete-button-class{
cursor: pointer;
color: #646a73
}
// 添加按钮样式
.add-button-class{
cursor: pointer;
border: 0 solid;
//width: 105px;
height: 22px;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
display: flex;
align-items: center;
letter-spacing: -0.1px;
color: #3370FF;
.span-class{
vertical-align:2px;
color: #3370FF;
padding-left: 5px
}
}
button{
height: 32px;
min-width: 80px
}
.save-btn{
background-color: #3370FF;
}
.cancel-btn{
}
// 下方操作按钮区域
.footer {
border-top: 1px solid var(--el-border-color);
padding: 24px 0px 0px 0px;
display: flex;
justify-content: space-between;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
margin: 0px -50px 0px;
.footer-form {
min-width: 400px;
}
.footer-center {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.footer-btn {
margin: 0px 80px 0px;
text-align: right;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
}
}
.description {
padding-left: 15px;
font-size: smaller;
color: #606266;
}
}
// 自定义弹出框样式
.custom-dialog{
//标题样式
.el-dialog__header{
padding: 24px !important;
}
//关闭按钮样式
.el-dialog__headerbtn .el-dialog__close{
height: auto !important;
color: #646A73 !important;
font-size: x-large !important;
}
.el-dialog__headerbtn .el-dialog__close:hover{
background: rgba(31, 35, 41, 0.1) !important;
border-radius: 4px !important;
}
//内容间距
.el-dialog__body{
padding: 0px 24px 0px 24px;
}
.el-dialog__footer{
padding-bottom: 29px !important;
}
//下方按钮
.footer-btn{
button{
height: 32px;
min-width: 80px
}
.save-btn{
background-color: #3370FF;
}
.cancel-btn{
}
}
.mt-10 {
margin-top: 10px;
}
.custom-radio-group.el-radio-group{
border: 1px solid #BBBFC4;
border-radius: 5px;
height: 30px;
label{
border: 0px solid;
padding: 2px 10px 2px 4px;
}
.el-radio-button__inner{
padding: 4px;
border: 0px;
border-radius: 5px;
}
.el-radio-button{
height: auto;
}
.el-radio-button is-active{
height: auto;
}
.el-radio-button__original-radio:checked + .el-radio-button__inner{
color: #3370FF;
border: 0;
box-shadow: 0 0 0 0;
background: rgba(51, 112, 255, 0.1);
}
.ml-10 {
margin-left: 10px;
}
.mr-10 {
margin-right: 10px;
}
.mb-10 {
margin-bottom: 10px;
}
.mb-20 {
margin-bottom: 20px;
}
.p-15 {
padding: 15px;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.border-b {
border-bottom: 1px solid var(--el-border-color);
}
.border-r {
border-right: 1px solid var(--el-border-color);
}
.border-t {
border-top: 1px solid var(--el-border-color);
}
.border-b-light {
border-bottom: 1px solid var(--el-border-color-lighter);
}
.cursor{
cursor: pointer;
}
.main-calc-height {
height: calc(100vh - 125px);
}

View File

@ -1,21 +0,0 @@
// 抽屉样式整体修改
.el-drawer{
.el-drawer__header{
padding: 0;
margin: 0 24px;
height: 56px;
border-bottom: 1px solid #D5D6D8;
.el-drawer__title {
color: #1f2329;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
}
.el-drawer__body{
--el-drawer-padding-primary:24px
}
}

View File

@ -1,3 +1,11 @@
:root {
--el-menu-item-height: 45px;
}
.el-avatar {
--el-avatar-bg-color: var(--el-color-primary);
--el-avatar-size-small: 33px;
cursor: pointer;
}
.el-popper {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
@ -5,4 +13,32 @@
.el-form {
--el-form-inline-content-width: 100%;
}
.el-dialog {
.dialog-sub-title {
color: var(--el-text-color-regular);
margin: 5px 0;
}
.el-dialog__body {
padding: 15px var(--el-dialog-padding-primary) 10px !important;
}
}
// 抽屉样式整体修改
.el-drawer {
.el-drawer__header {
padding: 0;
margin: 0 24px;
height: 56px;
border-bottom: 1px solid var(--el-border-color);
.el-drawer__title {
color: #1f2329;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
}
.el-drawer__body {
--el-drawer-padding-primary: 24px;
}
}

View File

@ -1,4 +1,7 @@
@use "./variables/index.scss";
@use "./app.scss";
@use "./element-plus.scss";
@use "./drawer.scss";
@import 'element-plus/dist/index.css';
@import './variables.scss';
@import './app.scss';
@import './element-plus.scss';
@import 'nprogress/nprogress.css'

View File

@ -1,15 +0,0 @@
@mixin flex-row($justify: flex-start, $align: stretch) {
display: flex;
@if $justify != flex-start {
justify-content: $justify;
}
@if $align != stretch {
align-items: $align;
}
}
@mixin variant($color, $background-color, $border-color) {
color: $color;
background-color: $background-color;
border-color: $border-color;
}

View File

@ -0,0 +1,17 @@
:root {
--el-color-primary: rgba(51, 112, 255, 1);
--app-layout-bg-color: #f3f5f6;
--app-base-text-color: rgba(31, 35, 41, 1);
--app-view-padding: 15px;
--app-view-bg-color: #ffffff;
--hover-bg-color: #fafafa;
/** header 组件 */
--app-header-height: 56px;
--app-header-padding: 0 20px;
--app-header-bg-color: #ffffff;
/** sidebar 组件 */
--sidebar-bg-color: #ffffff;
--sidebar-width: 198px;
--team-manage-left-width : 280px;
}

View File

@ -1,7 +0,0 @@
:root{
--app-base-text-color:rgba(31, 35, 41, 1);
--app-base-text-font-size:14px;
--app-base-text-hover-color:rgba(51, 112, 255, 1);
--app-base-text-hover-bg-color:rgba(51, 112, 255, 0.1);
--app-base-action-text-color:var(--app-base-text-hover-color );
}

View File

@ -1,6 +0,0 @@
:root{
--app-header-height: 56px;
--app-header-padding: 0 20px;
--app-header-bg-color: #252b3c;
}

View File

@ -1,2 +0,0 @@
@use "./header.scss";
@use "./app.scss";

45
ui/src/utils/message.ts Normal file
View File

@ -0,0 +1,45 @@
import { ElMessageBox, ElMessage } from 'element-plus'
export const MsgSuccess = (message: string) => {
ElMessage.success({
message: message,
type: 'success',
showClose: true,
duration: 1500
})
}
export const MsgInfo = (message: string) => {
ElMessage.info({
message: message,
type: 'info',
showClose: true,
duration: 1500
})
}
export const MsgWarning = (message: string) => {
ElMessage.warning({
message: message,
type: 'warning',
showClose: true,
duration: 1500
})
}
export const MsgError = (message: string) => {
ElMessage.error({
message: message,
type: 'error',
showClose: true,
duration: 1500
})
}
export const MsgConfirm = (message: string, options = {}) => {
const defaultOptions: Object = {
type: 'warning',
...options
}
return ElMessageBox.confirm(message, '确认', defaultOptions)
}

View File

@ -1,15 +1,17 @@
import { store } from '@/stores'
import { useUserStore } from '@/stores/user'
import { Role, Permission, ComplexPermission } from '@/common/permission/type'
import useStore from '@/stores';
import { Role, Permission, ComplexPermission } from '@/utils/permission/type'
/**
*
* @param permission
* @returns True false
*/
const hasPermissionChild = (permission: Role | string | Permission | ComplexPermission) => {
const userStore = useUserStore(store)
const permissions = userStore.getPermissions()
const role = userStore.getRole()
const { user } = useStore();
const permissions = user.getPermissions()
const role = user.getRole()
if (!permission) {
return true
}
if (permission instanceof Role) {
return role === permission.role
}
@ -24,6 +26,7 @@ const hasPermissionChild = (permission: Role | string | Permission | ComplexPerm
if (typeof permission === 'string') {
return permissions.includes(permission)
}
return false
}
/**

View File

@ -0,0 +1,7 @@
<template>
<div>dataset 文档</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -1,4 +1,4 @@
<template >
<template>
<div>
dataset
</div>

View File

@ -1,16 +1,19 @@
<template >
<div>
<el-button v-hasPermission="'USER:DELETE'">用户删除权限</el-button>
<el-button v-hasPermission="'USER:READ'">用户只读权限</el-button>
<el-button v-hasPermission="new Role('USER')">普通用户角色</el-button>
<el-button v-hasPermission="[new Role('ADMIN'), new Role('USER')]">普通用户或者管理员</el-button>
<el-button
v-hasPermission="{ permission: ['USER:READ', new Role('USER')], compare: 'AND' }">普通角色并且用户只读权限</el-button>
<template>
<div>
<el-button v-hasPermission="'USER:DELETE'">用户删除权限</el-button>
<el-button v-hasPermission="'USER:READ'">用户只读权限</el-button>
<el-button v-hasPermission="new Role('USER')">普通用户角色</el-button>
<el-button v-hasPermission="[new Role('ADMIN'), new Role('USER')]"
>普通用户或者管理员</el-button
>
<el-button v-hasPermission="{ permission: ['USER:READ', new Role('USER')], compare: 'AND' }"
>普通角色并且用户只读权限</el-button
>
首页
</div>
首页
</div>
</template>
<script setup lang="ts">
import { Role } from "@/common/permission/type"
import { Role } from '@/utils/permission/type'
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>

View File

@ -1,169 +0,0 @@
<template>
<LoiginLayout>
<div class="register-form-container">
<div class="register-form-title">
<div class="title">
<div class="logo"></div>
<div>智能客服</div>
</div>
<div class="sub-title">忘记密码</div>
</div>
<el-form class="register-form" ref="resetPasswordFormRef" :model="CheckEmailForm" :rules="rules">
<el-form-item prop="email">
<el-input size="large" class="input-item" v-model="CheckEmailForm.email" placeholder="请输入邮箱">
<template #prepend>
<el-button :icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input size="large" class="code-input" v-model="CheckEmailForm.code" placeholder="请输入验证码">
<template #prepend>
<el-button :icon="Key" />
</template>
</el-input>
<el-button size="large" class="send-email-button" @click="sendEmail"
:loading="loading">获取验证码</el-button>
</el-form-item>
</el-form>
<el-button type="primary" class="register-button" @click="checkCode">立即验证</el-button>
<div class="operate-container">
<span class="register" @click="router.push('login')">&lt; 返回登陆</span>
</div>
</div>
</LoiginLayout>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { UserFilled, Key } from '@element-plus/icons-vue'
import type {
CheckCodeRequest
} from "@/api/user/type"
import LoiginLayout from "@/components/layout/login-layout/index.vue"
import { useRouter } from "vue-router"
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from "@/api/user/index"
import { ElMessage } from "element-plus"
const router = useRouter()
const CheckEmailForm = ref<CheckCodeRequest>({
email: "",
code: "",
type: 'reset_password'
});
const resetPasswordFormRef = ref<FormInstance>()
const rules = ref<FormRules<CheckCodeRequest>>({
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
if ((!emailRegExp.test(value) && value != '')) {
callback(new Error('请输入有效邮箱格式!'));
} else {
callback();
}
},
trigger: 'blur'
}
],
code: [
{ required: true, message: '请输入验证码' }
]
})
const loading = ref<boolean>(false)
const checkCode = () => {
resetPasswordFormRef.value?.validate()
.then(() => UserApi.checkCode(CheckEmailForm.value, loading))
.then(() => router.push({ name: 'reset_password', params: CheckEmailForm.value }))
}
/**
* 发送验证码
*/
const sendEmail = () => {
resetPasswordFormRef.value?.validateField("email", (v: boolean) => {
if (v) {
UserApi.sendEmit(CheckEmailForm.value.email, "reset_password", loading)
.then(() => {
ElMessage.success("发送验证码成功")
})
}
})
}
</script>
<style lang="scss" scope>
.register-form-container {
width: 420px;
.code-input {
width: 250px;
}
.send-email-button {
margin-left: 12px;
width: 158px;
}
.register-form-title {
width: 100%;
margin-bottom: 30px;
.title {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.logo {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
}
.sub-title {
color: #101010;
font-size: 18px;
}
}
.operate-container {
margin-top: 12px;
color: rgba(51, 112, 255, 1);
display: flex;
justify-content: space-between;
.register {
cursor: pointer;
}
.forgot-password {
cursor: pointer;
}
}
.register-button {
width: 100%;
margin-top: 20px;
height: 40px;
}
}
</style>

View File

@ -1,32 +0,0 @@
<script setup lang="ts">
import TopBar from "@/components/layout/top-bar/index.vue"
</script>
<template>
<div class="common-layout">
<el-container>
<el-header>
<TopBar></TopBar>
</el-header>
<el-main>
<RouterView></RouterView>
</el-main>
</el-container>
</div>
</template>
<style lang="scss" scoped>
.el-header {
--el-header-padding: 0;
--el-header-height: 56px;
padding: var(--el-header-padding);
box-sizing: border-box;
flex-shrink: 0;
height: var(--el-header-height);
}
.el-main {
--el-main-padding: 0;
width: 100vw;
height: calc(100vh - var(--el-header-height, 60px));
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<login-layout>
<LoginContainer>
<h3 class="mb-20">忘记密码</h3>
<el-form
class="register-form"
ref="resetPasswordFormRef"
:model="CheckEmailForm"
:rules="rules"
>
<el-form-item prop="email">
<el-input
size="large"
class="input-item"
v-model="CheckEmailForm.email"
placeholder="请输入邮箱"
>
<template #prepend>
<el-button icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<div class="flex-between w-full">
<el-input
size="large"
class="code-input"
v-model="CheckEmailForm.code"
placeholder="请输入验证码"
>
<template #prepend>
<el-button icon="Key" />
</template>
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
@click="sendEmail"
:loading="loading"
>获取验证码</el-button
>
</div>
</el-form-item>
</el-form>
<el-button type="primary" class="login-submit-button w-full" @click="checkCode"
>立即验证</el-button
>
<div class="operate-container mt-10">
<el-button
class="register"
@click="router.push('/login')"
link
type="primary"
icon="DArrowLeft"
>
返回登录
</el-button>
</div>
</LoginContainer>
</login-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { CheckCodeRequest } from '@/api/type/user'
import { useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user'
import { MsgSuccess } from '@/utils/message'
const router = useRouter()
const CheckEmailForm = ref<CheckCodeRequest>({
email: '',
code: '',
type: 'reset_password'
})
const resetPasswordFormRef = ref<FormInstance>()
const rules = ref<FormRules<CheckCodeRequest>>({
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
if (!emailRegExp.test(value) && value != '') {
callback(new Error('请输入有效邮箱格式!'))
} else {
callback()
}
},
trigger: 'blur'
}
],
code: [{ required: true, message: '请输入验证码' }]
})
const loading = ref<boolean>(false)
const checkCode = () => {
resetPasswordFormRef.value
?.validate()
.then(() => UserApi.checkCode(CheckEmailForm.value, loading))
.then(() => router.push({ name: 'reset_password', params: CheckEmailForm.value }))
}
/**
* 发送验证码
*/
const sendEmail = () => {
resetPasswordFormRef.value?.validateField('email', (v: boolean) => {
if (v) {
UserApi.sendEmit(CheckEmailForm.value.email, 'reset_password', loading).then(() => {
MsgSuccess('发送验证码成功')
})
}
})
}
</script>
<style lang="scss" scope>
@import '../index.scss';
</style>

View File

@ -0,0 +1,4 @@
.login-submit-button {
margin-top: 12px;
height: 40px;
}

View File

@ -1,152 +1,102 @@
<template>
<LoiginLayout v-loading="loading">
<div class="login-form-container">
<div class="login-form-title">
<div class="title">
<div class="logo"></div>
<div>智能客服</div>
</div>
<div class="sub-title">欢迎使用智能客服管理平台</div>
</div>
<el-form class="login-form" :rules="rules" :model="loginForm" ref="loginFormRef">
<el-form-item>
<el-input size="large" class="input-item" v-model="loginForm.username" placeholder="请输入用户名">
<template #prepend>
<el-button :icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input type="password" size="large" class="input-item" v-model="loginForm.password"
placeholder="请输入密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
</el-form>
<div class="operate-container">
<span class="register" @click="router.push('register')">注册</span>
<span class="forgot-password" @click="router.push('forgot_password')">忘记密码</span>
</div>
<el-button type="primary" class="login-button" @click="login">登录</el-button>
</div>
</LoiginLayout>
<login-layout v-loading="loading">
<LoginContainer subTitle="欢迎使用智能客服管理平台">
<el-form class="login-form" :rules="rules" :model="loginForm" ref="loginFormRef">
<el-form-item>
<el-input
size="large"
class="input-item"
v-model="loginForm.username"
placeholder="请输入用户名"
>
<template #prepend>
<el-button icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input
type="password"
size="large"
class="input-item"
v-model="loginForm.password"
placeholder="请输入密码"
show-password
>
<template #prepend>
<el-button icon="Lock" />
</template>
</el-input>
</el-form-item>
</el-form>
<div class="operate-container flex-between">
<el-button class="register" @click="router.push('/register')" link type="primary">
注册
</el-button>
<el-button
class="forgot-password"
@click="router.push('/forgot_password')"
link
type="primary"
>
忘记密码
</el-button>
</div>
<el-button type="primary" class="login-submit-button w-full" @click="login">登录</el-button>
</LoginContainer>
</login-layout>
</template>
<script setup lang="ts">
import { ref } from "vue"
import type { LoginRequest } from "@/api/user/type"
import { UserFilled, Lock } from '@element-plus/icons-vue'
import LoiginLayout from "@/components/layout/login-layout/index.vue"
import { useRouter } from "vue-router"
import { ref } from 'vue'
import type { LoginRequest } from '@/api/type/user'
import { useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import { useUserStore } from "@/stores/user"
import useStore from '@/stores'
const loading = ref<boolean>(false);
const userStore = useUserStore();
const loading = ref<boolean>(false)
const { user } = useStore()
const router = useRouter()
const loginForm = ref<LoginRequest>({
username: '',
password: ''
});
username: '',
password: ''
})
const rules = ref<FormRules<LoginRequest>>({
username: [
{
required: true,
message: "请输入用户名",
trigger: "blur",
},
],
password: [
{
required: true,
message: "请输入密码",
trigger: "blur",
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
],
username: [
{
required: true,
message: '请输入用户名',
trigger: 'blur'
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
}
]
})
const loginFormRef = ref<FormInstance>()
const login = () => {
loginFormRef.value?.validate().then(() => {
loading.value = true
userStore.login(loginForm.value.username, loginForm.value.password)
.then(() => { router.push({ name: 'home' }) })
.finally(() => loading.value = false)
})
loginFormRef.value?.validate().then(() => {
loading.value = true
user
.login(loginForm.value.username, loginForm.value.password)
.then(() => {
router.push({ name: 'home' })
})
.finally(() => (loading.value = false))
})
}
</script>
<style lang="scss" scope>
.login-form-container {
width: 420px;
.login-form-title {
width: 100%;
margin-bottom: 30px;
.title {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.logo {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
}
.sub-title {
display: flex;
align-items: center;
justify-content: center;
color: #101010;
font-size: 18px;
}
}
.operate-container {
color: rgba(51, 112, 255, 1);
display: flex;
justify-content: space-between;
.register {
cursor: pointer;
}
.forgot-password {
cursor: pointer;
}
}
.login-button {
width: 100%;
margin-top: 20px;
height: 40px;
}
}
</style>
@import './index.scss';
</style>

View File

@ -0,0 +1,203 @@
<template>
<login-layout>
<LoginContainer>
<h3 class="mb-20">注册</h3>
<el-form class="register-form" :model="registerForm" :rules="rules" ref="registerFormRef">
<el-form-item prop="username">
<el-input
size="large"
class="input-item"
v-model="registerForm.username"
placeholder="请输入用户名"
>
<template #prepend>
<el-button :icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="registerForm.password"
placeholder="请输入密码"
show-password
>
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input
type="password"
size="large"
class="input-item"
v-model="registerForm.re_password"
placeholder="请输入确认密码"
show-password
>
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="email">
<el-input
size="large"
class="input-item"
v-model="registerForm.email"
placeholder="请输入邮箱"
>
<template #prepend>
<el-button :icon="Message" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<div class="flex-between w-full">
<el-input
size="large"
class="code-input"
v-model="registerForm.code"
placeholder="请输入验证码"
>
<template #prepend>
<el-button :icon="Key" />
</template>
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
@click="sendEmail"
:loading="sendEmailLoading"
>获取验证码</el-button
>
</div>
</el-form-item>
</el-form>
<el-button type="primary" class="login-submit-button w-full" @click="register"
>注册</el-button
>
<div class="operate-container mt-10">
<el-button
class="register"
@click="router.push('/login')"
link
type="primary"
icon="DArrowLeft"
>
返回登录
</el-button>
</div>
</LoginContainer>
</login-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { RegisterRequest } from '@/api/type/user'
import { UserFilled, Lock, Message, Key } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import UserApi from '@/api/user'
import { MsgSuccess } from '@/utils/message'
import type { FormInstance, FormRules } from 'element-plus'
const router = useRouter()
const registerForm = ref<RegisterRequest>({
username: '',
password: '',
re_password: '',
email: '',
code: ''
})
const rules = ref<FormRules<RegisterRequest>>({
username: [
{
required: true,
message: '请输入用户名',
trigger: 'blur'
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
}
],
re_password: [
{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
if (registerForm.value.password != registerForm.value.re_password) {
callback(new Error('密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
if (!emailRegExp.test(value) && value != '') {
callback(new Error('请输入有效邮箱格式!'))
} else {
callback()
}
},
trigger: 'blur'
}
],
code: [{ required: true, message: '请输入验证码' }]
})
const registerFormRef = ref<FormInstance>()
const register = () => {
registerFormRef.value
?.validate()
.then(() => {
return UserApi.register(registerForm.value)
})
.then(() => {
router.push('login')
})
}
const sendEmailLoading = ref<boolean>(false)
/**
* 发送验证码
*/
const sendEmail = () => {
registerFormRef.value?.validateField('email', (v: boolean) => {
if (v) {
UserApi.sendEmit(registerForm.value.email, 'register', sendEmailLoading).then(() => {
MsgSuccess('发送验证码成功')
})
}
})
}
</script>
<style lang="scss" scope>
@import '../index.scss';
</style>

View File

@ -0,0 +1,136 @@
<template>
<login-layout>
<LoginContainer>
<h3 class="mb-20">修改密码</h3>
<el-form
class="reset-password-form"
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="rules"
>
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.password"
placeholder="请输入密码"
show-password
>
<template #prepend>
<el-button icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="re_password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.re_password"
placeholder="请输入确认密码"
show-password
>
<template #prepend>
<el-button icon="Lock" />
</template>
</el-input>
</el-form-item>
</el-form>
<el-button type="primary" class="login-submit-button w-full" @click="resetPassword"
>确认修改</el-button
>
<div class="operate-container mt-10">
<el-button
class="register"
@click="router.push('/login')"
link
type="primary"
icon="DArrowLeft"
>
返回登录
</el-button>
</div>
</LoginContainer>
</login-layout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { ResetPasswordRequest } from '@/api/type/user'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user'
const router = useRouter()
const route = useRoute()
const resetPasswordForm = ref<ResetPasswordRequest>({
password: '',
re_password: '',
email: '',
code: ''
})
onMounted(() => {
const code = route.params.code
const email = route.params.email
if (code && email) {
resetPasswordForm.value.code = code as string
resetPasswordForm.value.email = email as string
} else {
router.push('forgot_password')
}
})
const rules = ref<FormRules<ResetPasswordRequest>>({
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
}
],
re_password: [
{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: '长度在 6 到 30 个字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error('密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const resetPasswordFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const resetPassword = () => {
resetPasswordFormRef.value
?.validate()
.then(() => UserApi.resetPassword(resetPasswordForm.value, loading))
.then(() => {
MsgSuccess('修改密码成功')
router.push({ name: 'login' })
})
}
</script>
<style lang="scss" scope>
@import '../index.scss';
</style>

View File

@ -1,236 +0,0 @@
<template>
<LoiginLayout>
<div class="register-form-container">
<div class="register-form-title">
<div class="title">
<div class="logo"></div>
<div>智能客服</div>
</div>
<div class="sub-title">修改密码</div>
</div>
<el-form class="register-form" :model="registerForm" :rules="rules" ref="registerFormRef">
<el-form-item prop="username">
<el-input size="large" class="input-item" v-model="registerForm.username" placeholder="请输入用户名">
<template #prepend>
<el-button :icon="UserFilled" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" size="large" class="input-item" v-model="registerForm.password"
placeholder="请输入密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input type="password" size="large" class="input-item" v-model="registerForm.re_password"
placeholder="请输入确认密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="email">
<el-input size="large" class="input-item" v-model="registerForm.email" placeholder="请输入邮箱">
<template #prepend>
<el-button :icon="Message" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input size="large" class="code-input" v-model="registerForm.code" placeholder="请输入验证码">
<template #prepend>
<el-button :icon="Key" />
</template>
</el-input>
<el-button size="large" class="send-email-button" @click="sendEmail"
:loading="sendEmailLoading">获取验证码</el-button>
</el-form-item>
</el-form>
<el-button type="primary" class="register-button" @click="register">注册</el-button>
<div class="operate-container">
<span class="register" @click="router.push('login')">&lt; 返回登陆</span>
</div>
</div>
</LoiginLayout>
</template>
<script setup lang="ts">
import { ref } from "vue"
import type { RegisterRequest } from "@/api/user/type"
import { UserFilled, Lock, Message, Key } from '@element-plus/icons-vue'
import LoiginLayout from "@/components/layout/login-layout/index.vue"
import { useRouter } from "vue-router"
import UserApi from "@/api/user/index"
import { ElMessage } from "element-plus"
import type { FormInstance, FormRules } from 'element-plus'
const router = useRouter()
const registerForm = ref<RegisterRequest>({
username: '',
password: '',
re_password: '',
email: '',
code: ''
});
const rules = ref<FormRules<RegisterRequest>>({
username: [
{
required: true,
message: "请输入用户名",
trigger: "blur",
},
],
password: [
{
required: true,
message: "请输入密码",
trigger: "blur",
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
],
re_password: [{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
{
validator: (rule, value, callback) => {
if (registerForm.value.password != registerForm.value.re_password) {
callback(new Error('密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
if ((!emailRegExp.test(value) && value != '')) {
callback(new Error('请输入有效邮箱格式!'));
} else {
callback();
}
},
trigger: 'blur'
}
],
code: [
{ required: true, message: '请输入验证码' }
]
})
const registerFormRef = ref<FormInstance>();
const register = () => {
registerFormRef.value?.validate().then(() => {
return UserApi.register(registerForm.value)
}).then(() => {
router.push("login")
})
}
const sendEmailLoading = ref<boolean>(false);
/**
* 发送验证码
*/
const sendEmail = () => {
registerFormRef.value?.validateField("email", (v: boolean) => {
if (v) {
UserApi.sendEmit(registerForm.value.email, "register",sendEmailLoading)
.then(() => {
ElMessage.success("发送验证码成功")
})
}
})
}
</script>
<style lang="scss" scope>
.register-form-container {
width: 420px;
.code-input {
width: 250px;
}
.send-email-button {
margin-left: 12px;
width: 158px;
}
.register-form-title {
width: 100%;
margin-bottom: 30px;
.title {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.logo {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
}
.sub-title {
color: #101010;
font-size: 18px;
}
}
.operate-container {
margin-top: 12px;
color: rgba(51, 112, 255, 1);
display: flex;
justify-content: space-between;
.register {
cursor: pointer;
}
.forgot-password {
cursor: pointer;
}
}
.register-button {
width: 100%;
margin-top: 20px;
height: 40px;
}
}
</style>

View File

@ -1,183 +0,0 @@
<template>
<LoiginLayout>
<div class="register-form-container">
<div class="register-form-title">
<div class="title">
<div class="logo"></div>
<div>智能客服</div>
</div>
<div class="sub-title">修改密码</div>
</div>
<el-form class="reset-password-form" ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="rules">
<el-form-item prop="password">
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.password"
placeholder="请输入密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="re_password">
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.re_password"
placeholder="请输入确认密码">
<template #prepend>
<el-button :icon="Lock" />
</template>
</el-input>
</el-form-item>
</el-form>
<el-button type="primary" class="register-button" @click="resetPassword">确认修改</el-button>
<div class="operate-container">
<span class="register" @click="router.push('login')">&lt; 返回登陆</span>
</div>
</div>
</LoiginLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue"
import type { ResetPasswordRequest } from "@/api/user/type"
import { Lock } from '@element-plus/icons-vue'
import LoiginLayout from "@/components/layout/login-layout/index.vue"
import { useRouter, useRoute } from "vue-router"
import { ElMessage } from "element-plus"
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from "@/api/user/index"
const router = useRouter()
const route = useRoute()
const resetPasswordForm = ref<ResetPasswordRequest>({
password: '',
re_password: '',
email: '',
code: ''
});
onMounted(() => {
const code = route.params.code;
const email = route.params.email;
if (code && email) {
resetPasswordForm.value.code = code as string;
resetPasswordForm.value.email = email as string;
} else {
router.push('forgot_password')
}
})
const rules = ref<FormRules<ResetPasswordRequest>>({
password: [
{
required: true,
message: "请输入密码",
trigger: "blur",
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
],
re_password: [{
required: true,
message: '请输入确认密码',
trigger: 'blur'
},
{
min: 6,
max: 30,
message: "长度在 6 到 30 个字符",
trigger: "blur",
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error('密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}],
})
const resetPasswordFormRef = ref<FormInstance>();
const loading = ref<boolean>(false);
const resetPassword = () => {
resetPasswordFormRef.value?.validate()
.then(() => UserApi.resetPassword(resetPasswordForm.value, loading))
.then(() => {
ElMessage.success("修改密码成功")
router.push({ name: 'login' })
})
}
</script>
<style lang="scss" scope>
.register-form-container {
width: 420px;
.code-input {
width: 250px;
}
.send-email-button {
margin-left: 12px;
width: 158px;
}
.register-form-title {
width: 100%;
margin-bottom: 30px;
.title {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.logo {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 48px;
height: 48px;
}
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
}
.sub-title {
color: #101010;
font-size: 18px;
}
}
.operate-container {
margin-top: 12px;
color: rgba(51, 112, 255, 1);
display: flex;
justify-content: space-between;
.register {
cursor: pointer;
}
.forgot-password {
cursor: pointer;
}
}
.register-button {
width: 100%;
margin-top: 20px;
height: 40px;
}
}
</style>

View File

@ -0,0 +1,102 @@
<template>
<el-dialog
v-model="dialogVisible"
:close-on-press-escape="false"
:close-on-click-modal="false"
:destroy-on-close="true"
width="600"
>
<template #header="{ titleId, titleClass }">
<h4 :id="titleId" :class="titleClass">添加成员</h4>
<div class="dialog-sub-title">成员登录后可以访问到您授权的数据</div>
</template>
<el-form
ref="addMemberFormRef"
:model="memberForm"
label-position="top"
:rules="rules"
@submit.prevent
>
<el-form-item label="用户名/邮箱" prop="users">
<tags-input
v-model:tags="memberForm.users"
v-model:tag="memberForm.user"
placeholder="请输入成员的用户名或邮箱,若需添加多个成员请使用回车分割。"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submitMember(addMemberFormRef)"> 添加 </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { MsgSuccess } from '@/utils/message'
import TeamApi from '@/api/team'
const emit = defineEmits(['refresh'])
const dialogVisible = ref<boolean>(false)
const memberForm = ref({
users: [],
user: ''
})
const addMemberFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const validateUsers = (rule: any, value: any, callback: any) => {
if (value?.length == 0 && !memberForm.value.user) {
callback(new Error('请输入用户名/邮箱'))
} else {
callback()
}
}
const rules = ref<FormRules>({
users: [{ type: 'array', validator: validateUsers }]
})
watch(dialogVisible, (bool) => {
if (!bool) {
memberForm.value = {
users: [],
user: ''
}
}
})
const open = () => {
dialogVisible.value = true
}
const submitMember = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
loading.value = true
const obj: any = {
username_or_email: memberForm.value.users?.length
? memberForm.value.users.toString()
: memberForm.value.user
}
TeamApi.postCreatTeamMember(obj).then(() => {
MsgSuccess('提交成功')
emit('refresh')
dialogVisible.value = false
})
} else {
console.log('error submit!')
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scope></style>

View File

@ -0,0 +1,104 @@
<template>
<el-table :data="data" :max-height="tableHeight">
<el-table-column prop="name" label="数据集名称" />
<el-table-column label="管理" align="center">
<template #header>
<el-checkbox
v-model="allChecked[MANAGE]"
label="管理"
@change="handleCheckAllChange($event, MANAGE)"
/>
</template>
<template #default="{ row }">
<el-checkbox v-model="row.operate[MANAGE]" @change="checkedOperateChange(MANAGE, row)" />
</template>
</el-table-column>
<el-table-column label="使用" align="center">
<template #header>
<el-checkbox
v-model="allChecked[USE]"
label="使用"
@change="handleCheckAllChange($event, USE)"
/>
</template>
<template #default="{ row }">
<el-checkbox v-model="row.operate[USE]" @change="checkedOperateChange(USE, row)" />
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
const MANAGE = 'MANAGE'
const USE = 'USE'
const props = defineProps({
data: {
type: Array,
default: () => []
},
id: String
})
const emit = defineEmits(['update:data'])
const allChecked: any = ref({
[MANAGE]: false,
[USE]: false
})
const tableHeight = ref(100)
watch(
() => props.data,
(val) => {
Object.keys(allChecked.value).map((item) => {
allChecked.value[item] = compare(item)
})
emit('update:data', val)
},
{
deep: true
}
)
function handleCheckAllChange(val: string | number | boolean, Name: string | number) {
if (val) {
props.data.map((item: any) => {
item.operate[Name] = true
})
} else {
props.data.map((item: any) => {
item.operate[Name] = false
})
}
}
function checkedOperateChange(Name: string | number, row: any) {
if (Name === MANAGE) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[USE] = true
}
})
}
allChecked.value[Name] = compare(Name)
}
function compare(attrs: string | number) {
const filterData = props.data.filter((item: any) => item?.operate[attrs])
return props.data.length > 0 && filterData.length === props.data.length
}
onMounted(() => {
tableHeight.value = window.innerHeight - 300
window.onresize = () => {
return (() => {
tableHeight.value = window.innerHeight - 300
})()
}
Object.keys(allChecked.value).map((item) => {
allChecked.value[item] = compare(item)
})
})
</script>
<style lang="scss" scope></style>

View File

@ -1,9 +1,233 @@
<template >
<div>
setting
</div>
</template>
<script lang="ts" setup>
<template>
<LayoutContent header="团队管理">
<div class="team-manage flex main-calc-height">
<div class="team-member p-15 border-r">
<h3>团队成员</h3>
<div class="align-right">
<el-button type="primary" link @click="addMember">
<AppIcon iconName="app-add-users" class="add-user-icon" />添加成员
</el-button>
</div>
<div class="mt-10">
<el-input v-model="filterText" placeholder="请输入用户名搜索" suffix-icon="Search" />
</div>
<div class="member-list mt-10" v-loading="loading">
<el-scrollbar>
<ul v-if="filterMember.length > 0">
<template v-for="(item, index) in filterMember" :key="index">
<li
@click.prevent="clickMemberHandle(item.id)"
:class="currentUser === item.id ? 'active' : ''"
class="border-b-light flex-between p-15 cursor"
>
<div>
<span class="mr-10">{{ item.username }}</span>
<el-tag effect="dark" v-if="isManage(item.type)">所有者</el-tag>
<el-tag effect="dark" type="warning" v-else>用户</el-tag>
</div>
<el-dropdown trigger="click" v-if="!isManage(item.type)">
<span class="cursor">
<el-icon><MoreFilled /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.prevent="deleteMember(item.id)"
>移除</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
</template>
</ul>
<el-empty description="暂无数据" v-else />
</el-scrollbar>
</div>
</div>
<div class="permission-setting flex" v-loading="rLoading">
<div class="team-manage__table p-15">
<h3>权限设置</h3>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane
v-for="item in settingTags"
:key="item.value"
:label="item.label"
:name="item.value"
>
<PermissionSetting :data="item.data"></PermissionSetting>
</el-tab-pane>
</el-tabs>
</div>
<div class="team-manage__footer border-t p-15 flex">
<el-button type="primary" @click="submitPermissions">保存</el-button>
</div>
</div>
</div>
<CreateMemberDialog ref="CreateMemberRef" @refresh="refresh" />
</LayoutContent>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, watch } from 'vue'
import TeamApi from '@/api/team'
import type { TeamMember } from '@/api/type/team'
import CreateMemberDialog from './component/CreateMemberDialog.vue'
import PermissionSetting from './component/PermissionSetting.vue'
import { MsgSuccess } from '@/utils/message'
const DATASET = 'DATASET'
const CreateMemberRef = ref<InstanceType<typeof CreateMemberDialog>>()
const loading = ref(false)
const rLoading = ref(false)
const memberList = ref<TeamMember[]>([]) //
const filterMember = ref<TeamMember[]>([]) //
const currentUser = ref<String>('')
const filterText = ref('')
const activeName = ref(DATASET)
const settingTags = reactive([
{
label: '数据集',
value: DATASET,
data: [] as any
},
{
label: '应用',
value: 'application',
data: [] as any
}
])
watch(filterText, (val) => {
if (val) {
filterMember.value = memberList.value.filter((v) => v.username.includes(val))
} else {
filterMember.value = memberList.value
}
})
function submitPermissions() {
rLoading.value = true
const obj: any = {
team_member_permission_list: []
}
settingTags.map((item) => {
item.data.map((v: any) => {
obj['team_member_permission_list'].push({
target_id: v.id,
type: v.type,
operate: v.operate
})
})
})
TeamApi.putMemberPermissions(currentUser.value, obj)
.then(() => {
MsgSuccess('提交成功')
MemberPermissions(currentUser.value)
})
.catch(() => {
rLoading.value = false
})
}
function MemberPermissions(id: String) {
rLoading.value = true
TeamApi.getMemberPermissions(id)
.then((res) => {
if (!res.data || Object.keys(res.data).length > 0) {
settingTags.map((item) => {
if (Object.keys(res.data).indexOf(item.value) !== -1) {
item.data = res.data[item.value]
}
})
}
rLoading.value = false
})
.catch(() => {
rLoading.value = false
})
}
function deleteMember(id: String) {
loading.value = true
TeamApi.delTeamMember(id)
.then(() => {
MsgSuccess('删除成功')
getMember()
})
.catch(() => {
loading.value = false
})
}
function isManage(type: String) {
return type === 'manage'
}
function clickMemberHandle(id: String) {
currentUser.value = id
MemberPermissions(id)
}
function addMember() {
CreateMemberRef.value?.open()
}
function getMember() {
loading.value = true
TeamApi.getTeamMember()
.then((res) => {
memberList.value = res.data
filterMember.value = res.data
currentUser.value = memberList.value[0].id
MemberPermissions(currentUser.value)
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function refresh() {
getMember()
}
onMounted(() => {
getMember()
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.team-manage {
.add-user-icon {
margin-right: 5px;
font-size: 20px;
}
.team-member {
box-sizing: border-box;
width: var(--team-manage-left-width);
min-width: var(--team-manage-left-width);
.member-list {
li {
&.active {
background: var(--el-color-primary-light-9);
}
}
}
}
.permission-setting {
box-sizing: border-box;
width: calc(100% - var(--team-manage-left-width) - 5px);
flex-direction: column;
}
.team-manage__table {
flex: 1;
}
.team-manage__footer {
flex: 0 0 auto;
justify-content: right;
}
}
</style>

View File

@ -1,7 +1,10 @@
import { fileURLToPath, URL } from 'node:url'
import type { ProxyOptions } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import DefineOptions from 'unplugin-vue-define-options/vite'
const envDir = './env'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
@ -13,10 +16,11 @@ export default defineConfig(({ mode }) => {
rewrite: (path) => path.replace(ENV.VITE_BASE_PATH, '/')
}
return {
preflight: false,
lintOnSave: false,
base: ENV.VITE_BASE_PATH,
envDir: envDir,
plugins: [vue()],
plugins: [vue(), DefineOptions()],
server: {
cors: true,
host: '0.0.0.0',