mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2025-12-28 04:00:22 +08:00
Compare commits
237 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
841bab0d63 | ||
|
|
453148fc8d | ||
|
|
7ec078bd7a | ||
|
|
dd0bc3e6e8 | ||
|
|
8c665c727b | ||
|
|
da98aa1c7d | ||
|
|
51b47ea722 | ||
|
|
220a7800f7 | ||
|
|
230e3a72d9 | ||
|
|
0cefadc2a5 | ||
|
|
2f1b747243 | ||
|
|
296d5ea6f0 | ||
|
|
3a415703d4 | ||
|
|
006f730457 | ||
|
|
606c5a2df0 | ||
|
|
30c375cc1d | ||
|
|
ddf14053da | ||
|
|
38edbcb68a | ||
|
|
3e54a82abb | ||
|
|
df6225a752 | ||
|
|
63c1f2f132 | ||
|
|
0bb2a904e7 | ||
|
|
ef3aaa5be5 | ||
|
|
869a68812c | ||
|
|
fd0032e0e9 | ||
|
|
b53d7daaa1 | ||
|
|
856bdfd0ee | ||
|
|
9f9884759c | ||
|
|
7dad43d003 | ||
|
|
7762e02b31 | ||
|
|
e5768fa1e3 | ||
|
|
7ee613d8cf | ||
|
|
80a5b7f053 | ||
|
|
eb160731da | ||
|
|
789231a7f4 | ||
|
|
6ea6e1c267 | ||
|
|
d971e7e4ba | ||
|
|
215998dc66 | ||
|
|
40f9ac1a6b | ||
|
|
6ec5588ed4 | ||
|
|
380e5768c4 | ||
|
|
5856f601fa | ||
|
|
d10b8f0e96 | ||
|
|
3860cf9ebb | ||
|
|
94b46d9bf6 | ||
|
|
4df7d44bf1 | ||
|
|
42b8aca37b | ||
|
|
0c96d0e937 | ||
|
|
b540f5599f | ||
|
|
18b8a81640 | ||
|
|
06b3afc2de | ||
|
|
3088773ebe | ||
|
|
83b42bf6b8 | ||
|
|
fd08d25ccf | ||
|
|
76c3f0b64c | ||
|
|
a1db8273f5 | ||
|
|
f5ab04112f | ||
|
|
805b2e066f | ||
|
|
6979b245a9 | ||
|
|
dff8862c75 | ||
|
|
1da5e8d573 | ||
|
|
7f97dd2f5a | ||
|
|
1f69f07100 | ||
|
|
f97beeb54b | ||
|
|
57bc68e7b0 | ||
|
|
90aa54d4a4 | ||
|
|
7564f115d6 | ||
|
|
8d3753a80e | ||
|
|
a816028560 | ||
|
|
acde2c1004 | ||
|
|
cb5dd34e17 | ||
|
|
73c82520ca | ||
|
|
e465ee50bf | ||
|
|
2be3f095aa | ||
|
|
26ecafffdc | ||
|
|
7150d93394 | ||
|
|
6e26769679 | ||
|
|
b7ce7912a7 | ||
|
|
1fa9d4d472 | ||
|
|
fa11b1bc64 | ||
|
|
4bf8916fdc | ||
|
|
8496c08646 | ||
|
|
065868a40b | ||
|
|
dd4cd871ba | ||
|
|
a2b84d35f7 | ||
|
|
e128dfabc7 | ||
|
|
21c1d6d3aa | ||
|
|
3990d4da80 | ||
|
|
ac9ccbadf0 | ||
|
|
8ae4046285 | ||
|
|
f88b4f52a1 | ||
|
|
00ba77c15e | ||
|
|
39a80926bf | ||
|
|
16957a96b7 | ||
|
|
ef33b28492 | ||
|
|
ae43ffb94f | ||
|
|
f0b6ce7d20 | ||
|
|
c3354afa6c | ||
|
|
08ef914528 | ||
|
|
d86ee26ad6 | ||
|
|
c8495f7a5f | ||
|
|
0636ac4716 | ||
|
|
67d966e096 | ||
|
|
b5ac614943 | ||
|
|
9151b2d297 | ||
|
|
84f8431134 | ||
|
|
fdc49f6dcc | ||
|
|
d2b88a8300 | ||
|
|
ffc042167a | ||
|
|
85f9c91d6e | ||
|
|
21391b202f | ||
|
|
36ddb23db6 | ||
|
|
f3c391c031 | ||
|
|
df378f784b | ||
|
|
2154267615 | ||
|
|
3203a9a459 | ||
|
|
5ce2150706 | ||
|
|
5bd380037c | ||
|
|
e63e9f5cf2 | ||
|
|
74c244cf37 | ||
|
|
5cd85cf72d | ||
|
|
96d88a97f1 | ||
|
|
00c32a950a | ||
|
|
bfd048d40a | ||
|
|
1254a199d7 | ||
|
|
1190d08a87 | ||
|
|
958589edd0 | ||
|
|
2338ded165 | ||
|
|
4d6a58bfc8 | ||
|
|
f15e21b0a0 | ||
|
|
f88820b727 | ||
|
|
598d256be4 | ||
|
|
5b51cfb4f1 | ||
|
|
45c2e3aebe | ||
|
|
c2249d531f | ||
|
|
44c6b420d0 | ||
|
|
117a46a251 | ||
|
|
d8569a4eb1 | ||
|
|
c268b3c75d | ||
|
|
21e0d86fcd | ||
|
|
76bd414941 | ||
|
|
894b87426a | ||
|
|
f9c2362cd8 | ||
|
|
d922dcc224 | ||
|
|
7c8a17bbb2 | ||
|
|
321e19a3a5 | ||
|
|
bf2d45416f | ||
|
|
cf1b83d3f1 | ||
|
|
bf63fb5ab7 | ||
|
|
a6f86ee315 | ||
|
|
b2cf78b36d | ||
|
|
c9c0c35343 | ||
|
|
3c46d2c159 | ||
|
|
585bf4a4c4 | ||
|
|
2bd85e6e60 | ||
|
|
967ae1c483 | ||
|
|
238bceb500 | ||
|
|
c2145c0ddb | ||
|
|
d759c9b9ae | ||
|
|
3fdba613d3 | ||
|
|
40d5106c6b | ||
|
|
094a9dcb3b | ||
|
|
db5089d92e | ||
|
|
a41ccad2d0 | ||
|
|
8973e39566 | ||
|
|
8c1191ece2 | ||
|
|
b3aa8147b1 | ||
|
|
0d240f083a | ||
|
|
c180cf54a8 | ||
|
|
2541706ac3 | ||
|
|
44b935e8f6 | ||
|
|
ea1ce0601a | ||
|
|
2989ecf126 | ||
|
|
ce94bf38d1 | ||
|
|
13bc185926 | ||
|
|
51cfd3e2eb | ||
|
|
c22cb3b35c | ||
|
|
621a2304e7 | ||
|
|
729337cdc5 | ||
|
|
4ef58b612f | ||
|
|
ba5d32244f | ||
|
|
efc2a194a3 | ||
|
|
6160c2e664 | ||
|
|
fbd1e9a38a | ||
|
|
17928cbc57 | ||
|
|
e7fc403c77 | ||
|
|
3f7ed95fdb | ||
|
|
7478f193f9 | ||
|
|
ba49d94bf4 | ||
|
|
ea9851ccd3 | ||
|
|
ec55f33655 | ||
|
|
7a85c714cb | ||
|
|
7b90d7f8de | ||
|
|
16f580c96d | ||
|
|
f1329a46e4 | ||
|
|
ef6df57dc5 | ||
|
|
95e5cd7134 | ||
|
|
9db7aa50a1 | ||
|
|
a9997984d5 | ||
|
|
acb47a17b4 | ||
|
|
9c5f4eaa3d | ||
|
|
361fb52345 | ||
|
|
5993e8d7d0 | ||
|
|
8648f16ed8 | ||
|
|
33aaadba60 | ||
|
|
437d87f19e | ||
|
|
dfcc8c2158 | ||
|
|
51a583fc1e | ||
|
|
396428104a | ||
|
|
ff2c25ff75 | ||
|
|
4f78bcb77c | ||
|
|
f62f59720d | ||
|
|
f296490569 | ||
|
|
606334ffad | ||
|
|
a44be5aec0 | ||
|
|
28f0815211 | ||
|
|
2f592f9570 | ||
|
|
d0c5a805d8 | ||
|
|
4929bf7d03 | ||
|
|
5fcc47816c | ||
|
|
8bcbcac531 | ||
|
|
402c7db7ba | ||
|
|
d955f55f7c | ||
|
|
b5fde56177 | ||
|
|
c761501855 | ||
|
|
8e04bc7821 | ||
|
|
d7a61db339 | ||
|
|
39da2634d5 | ||
|
|
b569b58c9d | ||
|
|
525d379af3 | ||
|
|
59cf11be7a | ||
|
|
5eb6874754 | ||
|
|
96644f14f5 | ||
|
|
a240985ee7 | ||
|
|
741524ffac | ||
|
|
d94a7a5a05 | ||
|
|
9f2126835a |
45
.cz-config.js
Normal file
45
.cz-config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
module.exports = {
|
||||||
|
types: [
|
||||||
|
{ value: 'feat', name:'feat: 新增功能' },
|
||||||
|
{ value: 'fix', name:'fix: 修复bug' },
|
||||||
|
{ value: 'docs', name:'docs: 文档变更' },
|
||||||
|
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
|
||||||
|
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
|
||||||
|
{ value: 'perf', name:'perf: 性能优化' },
|
||||||
|
{ value: 'test', name:'test: 添加、修改测试用例' },
|
||||||
|
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
|
||||||
|
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
|
||||||
|
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
|
||||||
|
{ value: 'revert', name:'revert: 回滚 commit' },
|
||||||
|
{ value: 'wip', name:'wip: 开发中' },
|
||||||
|
{ value: 'mod', name:'mod: 不确定分类的修改' },
|
||||||
|
{ value: 'release', name:'release: 发布' },
|
||||||
|
],
|
||||||
|
scopes: [
|
||||||
|
['custom', '自定义'],
|
||||||
|
['projects', '项目搭建'],
|
||||||
|
['components', '组件相关'],
|
||||||
|
['utils', 'utils 相关'],
|
||||||
|
['styles', '样式相关'],
|
||||||
|
['deps', '项目依赖'],
|
||||||
|
['other', '其他修改'],
|
||||||
|
].map(([value, description]) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
name: `${value.padEnd(30)} (${description})`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
messages: {
|
||||||
|
type: '确保本次提交遵循 Angular 规范!选择你要提交的类型:\n',
|
||||||
|
scope: '选择一个 scope(可选):',
|
||||||
|
customScope: '请输入自定义的 scope:',
|
||||||
|
subject: '填写简短精炼的变更描述:',
|
||||||
|
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
|
||||||
|
breaking: '列举非兼容性重大的变更(可选):',
|
||||||
|
footer: '列举出所有变更的 Issues Closed(可选)。 例如: #31, #34:',
|
||||||
|
confirmCommit: '确认提交?'
|
||||||
|
},
|
||||||
|
allowBreakingChanges: ['feat', 'fix'],
|
||||||
|
subjectLimit: 100,
|
||||||
|
breaklineChar: '|'
|
||||||
|
}
|
||||||
5
.env
5
.env
@@ -1,6 +1,3 @@
|
|||||||
VITE_APP_TITLE = 'Vue Naive Admin'
|
VITE_TITLE = 'Vue Naive Admin'
|
||||||
|
|
||||||
VITE_PORT = 3100
|
VITE_PORT = 3100
|
||||||
|
|
||||||
# 打包时自动生成CNAME文件,用于配置github pages自定义域名,如不需要可注释或者直接删除
|
|
||||||
VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
VITE_PUBLIC_PATH = '/'
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
# 是否启用MOCK
|
# 是否启用MOCK
|
||||||
VITE_APP_USE_MOCK = true
|
VITE_USE_MOCK = true
|
||||||
|
|
||||||
# proxy
|
# 是否启用MOCK
|
||||||
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]]
|
VITE_USE_PROXY = false
|
||||||
|
|
||||||
|
# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod'
|
||||||
|
VITE_PROXY_TYPE = 'dev'
|
||||||
|
|
||||||
# base api
|
# base api
|
||||||
VITE_APP_GLOB_BASE_API = '/api'
|
VITE_BASE_API = '/api'
|
||||||
|
|
||||||
# test base api
|
|
||||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
|
||||||
13
.env.github
Normal file
13
.env.github
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 自定义域名CNAME
|
||||||
|
# VITE_CNAME = 'template.qszone.com'
|
||||||
|
|
||||||
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
|
VITE_PUBLIC_PATH = '/vue-naive-admin/'
|
||||||
|
|
||||||
|
VITE_USE_HASH = true
|
||||||
|
|
||||||
|
# 是否启用MOCK
|
||||||
|
VITE_USE_MOCK = true
|
||||||
|
|
||||||
|
# base api
|
||||||
|
VITE_BASE_API = '/api'
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
VITE_PUBLIC_PATH = '/'
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
# 是否启用MOCK
|
# 是否启用MOCK
|
||||||
VITE_APP_USE_MOCK = true
|
VITE_USE_MOCK = true
|
||||||
|
|
||||||
# base api
|
# base api
|
||||||
VITE_APP_GLOB_BASE_API = '/api'
|
VITE_BASE_API = '/api'
|
||||||
|
|
||||||
# test base api
|
# 是否启用压缩
|
||||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
VITE_USE_COMPRESS = true
|
||||||
|
|
||||||
|
# 压缩类型
|
||||||
|
VITE_COMPRESS_TYPE = gzip
|
||||||
11
.env.staging
11
.env.staging
@@ -1,11 +0,0 @@
|
|||||||
# 资源公共路径,需要以 /开头和结尾
|
|
||||||
VITE_PUBLIC_PATH = '/'
|
|
||||||
|
|
||||||
# 是否启用MOCK
|
|
||||||
VITE_APP_USE_MOCK = false
|
|
||||||
|
|
||||||
# base api
|
|
||||||
VITE_APP_GLOB_BASE_API = 'http://localhost:8080/api'
|
|
||||||
|
|
||||||
# test base api
|
|
||||||
VITE_APP_GLOB_BASE_API_TEST = 'http://localhost:8080/api-test'
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
VITE_PUBLIC_PATH = '/'
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
# 是否启用MOCK
|
# 是否启用MOCK
|
||||||
VITE_APP_USE_MOCK = true
|
VITE_USE_MOCK = true
|
||||||
|
|
||||||
# base api
|
# base api
|
||||||
VITE_APP_GLOB_BASE_API = '/api'
|
VITE_BASE_API = '/api'
|
||||||
|
|
||||||
# test base api
|
|
||||||
VITE_APP_GLOB_BASE_API_TEST = '/api-test'
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
public
|
public
|
||||||
|
package.json
|
||||||
11
.eslintrc.js
11
.eslintrc.js
@@ -1,17 +1,6 @@
|
|||||||
// * https://zhuanlan.zhihu.com/p/388703150
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
|
||||||
browser: true, // browser global variables
|
|
||||||
node: true,
|
|
||||||
es2021: true, // adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
},
|
|
||||||
parser: 'vue-eslint-parser',
|
|
||||||
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
|
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
|
||||||
plugins: ['prettier'],
|
|
||||||
rules: {
|
rules: {
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'vue/valid-template-root': 'off',
|
'vue/valid-template-root': 'off',
|
||||||
|
|||||||
38
.github/workflows/deploy.yml
vendored
Normal file
38
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: use Node.js 16
|
||||||
|
uses: actions/setup-node@v2.1.2
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
|
||||||
|
- name: use pnpm 6.32.2
|
||||||
|
uses: pnpm/action-setup@v2.2.1
|
||||||
|
with:
|
||||||
|
version: 6.32.2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
pnpm run build:github
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
publish_dir: ./dist
|
||||||
|
github_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||||
|
user_name: ${{ secrets.USER_NAME }}
|
||||||
|
user_email: ${{ secrets.USER_EMAIL }}
|
||||||
|
force_orphan: true
|
||||||
|
commit_message: deploy gh-pages
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
*.local
|
*.local
|
||||||
|
stats.html
|
||||||
|
|||||||
36
.husky/_/husky.sh
Normal file
36
.husky/_/husky.sh
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
if [ -z "$husky_skip_init" ]; then
|
||||||
|
debug () {
|
||||||
|
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||||
|
echo "husky (debug) - $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly hook_name="$(basename -- "$0")"
|
||||||
|
debug "starting $hook_name..."
|
||||||
|
|
||||||
|
if [ "$HUSKY" = "0" ]; then
|
||||||
|
debug "HUSKY env variable is set to 0, skipping hook"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f ~/.huskyrc ]; then
|
||||||
|
debug "sourcing ~/.huskyrc"
|
||||||
|
. ~/.huskyrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
readonly husky_skip_init=1
|
||||||
|
export husky_skip_init
|
||||||
|
sh -e "$0" "$@"
|
||||||
|
exitCode="$?"
|
||||||
|
|
||||||
|
if [ $exitCode != 0 ]; then
|
||||||
|
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $exitCode = 127 ]; then
|
||||||
|
echo "husky - command not found in PATH=$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
fi
|
||||||
4
.husky/commit-msg
Normal file
4
.husky/commit-msg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx --no-install commitlint --edit "$1"
|
||||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint:staged
|
||||||
11
.vscode/extensions.json
vendored
11
.vscode/extensions.json
vendored
@@ -1,12 +1,9 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"vue.volar",
|
||||||
"johnsoncodehk.volar",
|
"antfu.iconify",
|
||||||
"hollowtree.vue-snippets",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"wayou.vscode-todo-highlight",
|
"sdras.vue-vscode-snippets",
|
||||||
"wayou.vscode-todo-highlight",
|
"cipchk.cssrem",
|
||||||
"aaron-bond.better-comments"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
34
.vscode/settings.json
vendored
34
.vscode/settings.json
vendored
@@ -1,26 +1,34 @@
|
|||||||
{
|
{
|
||||||
"files.eol": "\n",
|
|
||||||
"path-intellisense.mappings": {
|
"path-intellisense.mappings": {
|
||||||
"@/": "${workspaceRoot}/src"
|
"@/": "${workspaceRoot}/src",
|
||||||
|
"~/": "${workspaceRoot}"
|
||||||
},
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[jsonc]": {
|
"prettier.printWidth": 120,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"prettier.singleQuote": true,
|
||||||
},
|
"prettier.semi": false,
|
||||||
"[html]": {
|
"prettier.endOfLine": "lf",
|
||||||
"editor.defaultFormatter": "vscode.html-language-features"
|
"files.eol": "\n",
|
||||||
},
|
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.formatOnSave": false
|
||||||
},
|
},
|
||||||
"[css]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.formatOnSave": false
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.formatOnSave": false
|
||||||
},
|
},
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.formatOnSave": false
|
||||||
},
|
},
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript"]
|
"files.associations": {
|
||||||
|
"*.env.*": "dotenv",
|
||||||
|
"*.css": "postcss"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Ronnie Zhang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
127
README.md
127
README.md
@@ -1,74 +1,115 @@
|
|||||||
# VUE NAIVE ADMIN
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||||
|
<img alt="Vue Naive Admin Logo" width="200" src="https://assets.qszone.com/images/logo_qs.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 简介
|
<p align='center'>
|
||||||
|
<b>English</b> |
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.zh-CN.md">简体中文</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Vue Naive Admin,一个基于 Vue3.0、Vite、Naive UI 的后台管理模板,相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,没有集成 TypeScript,没有集成国际化,没有集成复杂的主题配置,学习成本非常低,对新手极其友好。不过麻雀虽小五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,经过参考多个 vue3 后台管理模板后以最简洁优雅的方式实现,非常适用于中小型项目或者个人项目,当然,以此模板进行二次封装改造用于大型项目也未尝不可。
|
### Introduction
|
||||||
|
|
||||||
## 为什么要开发这个模板
|
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin template,Based on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
|
||||||
|
|
||||||
1. Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
|
### Features
|
||||||
2. 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
|
|
||||||
3. 自己搭的模板开发起来才最顺手。本人很反感拿别人的模板直接上手开发,如果非要拿别人的模板开发也会尽量先吃透再用,不吃透就没有代码的掌控感和安全感
|
|
||||||
|
|
||||||
## 功能
|
- 🍒 Integrated [Naive UI](https://www.naiveui.com),recommended by Evan You.
|
||||||
|
- 🍑 Integrated login, logout and permission verification.
|
||||||
|
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
|
||||||
|
- 🍎 Integrated `eslint + prettier`.
|
||||||
|
- 🍌 Integrated `husky + commitlint`.
|
||||||
|
- 🍉 Integrated `Mock`.
|
||||||
|
- 🍍 Integrated `pinia`,lightweight, simple and easy to use alternative to vuex.
|
||||||
|
- 📦 Integrated `unplugin` auto import.
|
||||||
|
- 🤹 Integrated `iconify` icon,support custom svg icons.
|
||||||
|
- 🍇 Integrated `unocss`.
|
||||||
|
|
||||||
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,很香,https://www.naiveui.com
|
### Preview
|
||||||
- 🍑 集成登陆、注销及权限验证(暂只支持角色页面权限,后续考虑添加按钮权限)
|
|
||||||
- 🍐 集成多环境配置,dev、测试、预发布和生产
|
|
||||||
- 🍎 集成 eslint + prettier,代码约束和格式化统一
|
|
||||||
- 🍉 集成 Mock 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
|
||||||
- 🍇 集成 unocss,antfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的,很香
|
|
||||||
- 🍍 集成 pinia,Vuex 的替代方案,轻量、简单、易用,很香
|
|
||||||
- 🍏 集成 axios,支持多 axios 实例,支持线上环境免重新打包修改 baseURL
|
|
||||||
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
|
|
||||||
- 🍋 二次封装 localStorage 和 sessionStorage,支持设置过期时间
|
|
||||||
|
|
||||||
## 文档
|
[https://template.qszone.com](https://template.qszone.com)
|
||||||
|
|
||||||
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
|
||||||
|
|
||||||
## 构建步骤
|
### Docs
|
||||||
|
|
||||||
|
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||||
|
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
# Recommended setup git autocrlf 为 false
|
||||||
# 不清楚为什么要这样做的请参考这篇文章:https://www.freesion.com/article/4532642129
|
|
||||||
git config --global core.autocrlf false
|
git config --global core.autocrlf false
|
||||||
|
|
||||||
# 克隆项目
|
# Clone Project
|
||||||
git clone https://github.com/zclzone/vue-naive-admin.git
|
git clone https://github.com/zclzone/vue-naive-admin.git
|
||||||
|
|
||||||
# 进入项目目录
|
|
||||||
cd vue-naive-admin
|
cd vue-naive-admin
|
||||||
|
|
||||||
# 安装依赖
|
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
|
||||||
npm i
|
npm i -g pnpm # Installed and can be ignored
|
||||||
|
pnpm i # or npm i
|
||||||
|
|
||||||
# 启动
|
# Start
|
||||||
npm run dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 发布
|
### Build and Release
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 构建测试环境
|
# Test Environment
|
||||||
npm run build:test
|
pnpm build:test
|
||||||
|
|
||||||
# 构建预发布环境
|
# Github Environment
|
||||||
npm run build:staging
|
pnpm build:github
|
||||||
|
|
||||||
# 构建生产环境
|
# Prod Environment
|
||||||
npm run build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 其他指令
|
### Other
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# eslint代码格式检查
|
# eslint check
|
||||||
npm run lint
|
pnpm lint
|
||||||
|
|
||||||
# 代码检查并修复
|
# eslint check and fix
|
||||||
npm run lint:fix
|
pnpm lint:fix
|
||||||
|
|
||||||
# 预览发布包效果(需先执行构建指令)
|
# Preview(Need to build first)
|
||||||
npm run preview
|
pnpm preview
|
||||||
|
|
||||||
|
# Commit(husky+commitlint)
|
||||||
|
pnpm cz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TS version: Qs Admin
|
||||||
|
|
||||||
|
#### source code
|
||||||
|
|
||||||
|
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
|
||||||
|
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
|
||||||
|
|
||||||
|
#### preview
|
||||||
|
|
||||||
|
- [https://admin.qszone.com](https://admin.qszone.com)
|
||||||
|
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||||
|
|
||||||
|
### Communication group & About the author
|
||||||
|
|
||||||
|
<a href="https://blog.qszone.com/about/">
|
||||||
|
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
115
README.zh-CN.md
Normal file
115
README.zh-CN.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||||
|
<img alt="Vue Naive Admin Logo" width="200" src="https://assets.qszone.com/images/logo_qs.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/actions"><img allt="checks" src="https://badgen.net/github/checks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="stars" src="https://badgen.net/github/stars/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin"><img allt="forks" src="https://badgen.net/github/forks/zclzone/vue-naive-admin"/></a>
|
||||||
|
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align='center'>
|
||||||
|
<b>简体中文</b> |
|
||||||
|
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 简介
|
||||||
|
|
||||||
|
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **完全开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite3、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,学习成本非常低,非常适合中小型项目或者个人项目。
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 🍒 集成 [Naive UI](https://www.naiveui.com)
|
||||||
|
- 🍑 集成登陆、注销及权限验证
|
||||||
|
- 🍐 集成多环境配置,dev、测试、生产和github pages环境
|
||||||
|
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
|
||||||
|
- 🍌 集成 `husky + commitlint`,代码提交规范化
|
||||||
|
- 🍉 集成 `mock` 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||||
|
- 🍍 集成 `pinia`,vuex 的替代方案,轻量、简单、易用
|
||||||
|
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
|
||||||
|
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
|
||||||
|
- 🍇 集成 `unocss`,antfu 开源的原子 css 解决方案,非常轻量
|
||||||
|
|
||||||
|
### 预览
|
||||||
|
|
||||||
|
[https://template.qszone.com](https://template.qszone.com)
|
||||||
|
|
||||||
|
[https://zclzone.github.io/vue-naive-admin](https://zclzone.github.io/vue-naive-admin)
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||||
|
|
||||||
|
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||||
|
# 不清楚为什么要这样做的请参考这篇文章:https://www.freesion.com/article/4532642129
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/zclzone/vue-naive-admin.git
|
||||||
|
|
||||||
|
# 进入项目目录
|
||||||
|
cd vue-naive-admin
|
||||||
|
|
||||||
|
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
|
||||||
|
npm i -g pnpm # 装了可忽略
|
||||||
|
pnpm i # 或者 npm i
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建发布
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 构建测试环境
|
||||||
|
pnpm build:test
|
||||||
|
|
||||||
|
# 构建github pages环境
|
||||||
|
pnpm build:github
|
||||||
|
|
||||||
|
# 构建生产环境
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 其他指令
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# eslint代码格式检查
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# 代码检查并修复
|
||||||
|
pnpm lint:fix
|
||||||
|
|
||||||
|
# 预览发布包效果(需先执行构建指令)
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# 提交代码(husky+commitlint)
|
||||||
|
pnpm cz
|
||||||
|
```
|
||||||
|
|
||||||
|
### TS 版本: Qs Admin
|
||||||
|
|
||||||
|
#### 源码
|
||||||
|
|
||||||
|
- gitub: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
|
||||||
|
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
|
||||||
|
|
||||||
|
#### 预览
|
||||||
|
|
||||||
|
- [https://admin.qszone.com](https://admin.qszone.com)
|
||||||
|
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||||
|
|
||||||
|
### 入群交流 & 关于作者
|
||||||
|
|
||||||
|
<a href="https://blog.qszone.com/about/">
|
||||||
|
<img src="https://assets.qszone.com/images/about.png" style="max-width: 400px" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
13
build/config/define.js
Normal file
13
build/config/define.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 此处定义的是全局常量,启动或打包后将添加到window中
|
||||||
|
* https://vitejs.cn/config/#define
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 项目构建时间
|
||||||
|
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
||||||
|
|
||||||
|
export const viteDefine = {
|
||||||
|
_BUILD_TIME_,
|
||||||
|
}
|
||||||
2
build/config/index.js
Normal file
2
build/config/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './define'
|
||||||
|
export * from './proxy'
|
||||||
15
build/config/proxy.js
Normal file
15
build/config/proxy.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { getProxyConfig } from '../../settings'
|
||||||
|
|
||||||
|
export function createViteProxy(isUseProxy = true, proxyType) {
|
||||||
|
if (!isUseProxy) return undefined
|
||||||
|
|
||||||
|
const proxyConfig = getProxyConfig(proxyType)
|
||||||
|
const proxy = {
|
||||||
|
[proxyConfig.prefix]: {
|
||||||
|
target: proxyConfig.target,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(new RegExp(`^${proxyConfig.prefix}`), ''),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
@@ -1,3 +1 @@
|
|||||||
export const GLOB_CONFIG_FILE_NAME = 'app.config.js'
|
|
||||||
export const GLOB_CONFIG_NAME = '__APP__GLOB__CONF__'
|
|
||||||
export const OUTPUT_DIR = 'dist'
|
export const OUTPUT_DIR = 'dist'
|
||||||
|
|||||||
15
build/plugin/html.js
Normal file
15
build/plugin/html.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||||
|
|
||||||
|
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||||
|
const { VITE_TITLE } = viteEnv
|
||||||
|
|
||||||
|
const htmlPlugin = createHtmlPlugin({
|
||||||
|
minify: isBuild,
|
||||||
|
inject: {
|
||||||
|
data: {
|
||||||
|
title: VITE_TITLE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return htmlPlugin
|
||||||
|
}
|
||||||
46
build/plugin/index.js
Normal file
46
build/plugin/index.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * 扩展setup插件,支持在script标签中使用name属性
|
||||||
|
* usage: <script setup name="MyComp"></script>
|
||||||
|
*/
|
||||||
|
import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * unocss插件,原子css
|
||||||
|
* https://github.com/antfu/unocss
|
||||||
|
*/
|
||||||
|
import Unocss from 'unocss/vite'
|
||||||
|
|
||||||
|
// rollup打包分析插件
|
||||||
|
import visualizer from 'rollup-plugin-visualizer'
|
||||||
|
// 压缩
|
||||||
|
import viteCompression from 'vite-plugin-compression'
|
||||||
|
|
||||||
|
import { configHtmlPlugin } from './html'
|
||||||
|
import { configMockPlugin } from './mock'
|
||||||
|
import unplugin from './unplugin'
|
||||||
|
|
||||||
|
export function createVitePlugins(viteEnv, isBuild) {
|
||||||
|
const plugins = [vue(), vueSetupExtend(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||||
|
|
||||||
|
if (viteEnv?.VITE_USE_MOCK) {
|
||||||
|
plugins.push(configMockPlugin(isBuild))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viteEnv.VITE_USE_COMPRESS) {
|
||||||
|
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBuild) {
|
||||||
|
plugins.push(
|
||||||
|
visualizer({
|
||||||
|
open: true,
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
@@ -2,12 +2,11 @@ import { viteMockServe } from 'vite-plugin-mock'
|
|||||||
|
|
||||||
export function configMockPlugin(isBuild) {
|
export function configMockPlugin(isBuild) {
|
||||||
return viteMockServe({
|
return viteMockServe({
|
||||||
ignore: /^\_/,
|
mockPath: 'mock/api',
|
||||||
mockPath: 'mock',
|
|
||||||
localEnabled: !isBuild,
|
localEnabled: !isBuild,
|
||||||
prodEnabled: isBuild,
|
prodEnabled: isBuild,
|
||||||
injectCode: `
|
injectCode: `
|
||||||
import { setupProdMockServer } from '../mock/_createProdServer';
|
import { setupProdMockServer } from '../mock';
|
||||||
setupProdMockServer();
|
setupProdMockServer();
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
43
build/plugin/unplugin.js
Normal file
43
build/plugin/unplugin.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * unplugin-icons插件,自动引入iconify图标
|
||||||
|
* usage: https://github.com/antfu/unplugin-icons
|
||||||
|
* 图标库: https://icones.js.org/
|
||||||
|
*/
|
||||||
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||||
|
|
||||||
|
import { getSrcPath } from '../utils'
|
||||||
|
|
||||||
|
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||||
|
|
||||||
|
export default [
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router'],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
Icons({
|
||||||
|
compiler: 'vue3',
|
||||||
|
customCollections: {
|
||||||
|
custom: FileSystemIconLoader(customIconPath),
|
||||||
|
},
|
||||||
|
scale: 1,
|
||||||
|
defaultClass: 'inline-block',
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
iconDirs: [customIconPath],
|
||||||
|
symbolId: 'icon-custom-[dir]-[name]',
|
||||||
|
inject: 'body-last',
|
||||||
|
customDomId: '__CUSTOM_SVG_ICON__',
|
||||||
|
}),
|
||||||
|
]
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { writeFileSync } from 'fs-extra'
|
import { writeFileSync } from 'fs-extra'
|
||||||
import { OUTPUT_DIR } from '../constant'
|
import { OUTPUT_DIR } from '../constant'
|
||||||
import { getEnvConfig, getRootPath } from '../utils'
|
import { getEnvConfig, getRootPath } from '../utils'
|
||||||
|
|
||||||
export function runBuildCNAME() {
|
export function runBuildCNAME() {
|
||||||
const { VITE_APP_GLOB_CNAME } = getEnvConfig()
|
const { VITE_CNAME } = getEnvConfig()
|
||||||
if (!VITE_APP_GLOB_CNAME) return
|
if (!VITE_CNAME) return
|
||||||
try {
|
try {
|
||||||
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_GLOB_CNAME)
|
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(chalk.red('CNAME file failed to package:\n' + error))
|
console.log(chalk.red('CNAME file failed to package:\n' + error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { GLOB_CONFIG_FILE_NAME, GLOB_CONFIG_NAME, OUTPUT_DIR } from '../constant'
|
|
||||||
import fs, { writeFileSync } from 'fs-extra'
|
|
||||||
import chalk from 'chalk'
|
|
||||||
import { getEnvConfig, getRootPath } from '../utils'
|
|
||||||
|
|
||||||
function createConfig(option) {
|
|
||||||
const { config, configName, configFileName } = option
|
|
||||||
try {
|
|
||||||
const windowConf = `window.${configName}`
|
|
||||||
const configStr = `${windowConf}=${JSON.stringify(config)};
|
|
||||||
Object.freeze(${windowConf});
|
|
||||||
Object.defineProperty(window, "${configName}", {
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
`.replace(/\s/g, '')
|
|
||||||
fs.mkdirp(getRootPath(OUTPUT_DIR))
|
|
||||||
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(chalk.red('configuration file configuration file failed to package:\n' + error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runBuildConfig() {
|
|
||||||
const config = getEnvConfig()
|
|
||||||
const configName = GLOB_CONFIG_NAME
|
|
||||||
const configFileName = GLOB_CONFIG_FILE_NAME
|
|
||||||
createConfig({ config, configName, configFileName })
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { runBuildConfig } from './build-config'
|
|
||||||
import { runBuildCNAME } from './build-cname'
|
import { runBuildCNAME } from './build-cname'
|
||||||
|
|
||||||
export const runBuild = async () => {
|
export const runBuild = async () => {
|
||||||
try {
|
try {
|
||||||
runBuildConfig()
|
|
||||||
runBuildCNAME()
|
runBuildCNAME()
|
||||||
console.log(`✨ ${chalk.cyan('build successfully!')}`)
|
console.log(`✨ ${chalk.cyan('build successfully!')}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,33 +2,38 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
export function wrapperEnv(envOptions) {
|
/**
|
||||||
if (!envOptions) return {}
|
* * 项目根路径
|
||||||
const ret = {}
|
* @descrition 结尾不带/
|
||||||
|
*/
|
||||||
|
export function getRootPath() {
|
||||||
|
return path.resolve(process.cwd())
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in envOptions) {
|
/**
|
||||||
let val = envOptions[key]
|
* * 项目src路径
|
||||||
if (['true', 'false'].includes(val)) {
|
* @param srcName src目录名称(默认: "src")
|
||||||
val = val === 'true'
|
* @descrition 结尾不带斜杠
|
||||||
}
|
*/
|
||||||
if (['VITE_PORT'].includes(key)) {
|
export function getSrcPath(srcName = 'src') {
|
||||||
val = +val
|
return path.resolve(getRootPath(), srcName)
|
||||||
}
|
}
|
||||||
if (key === 'VITE_PROXY' && val) {
|
|
||||||
try {
|
const httpsReg = /^https:\/\//
|
||||||
val = JSON.parse(val.replace(/'/g, '"'))
|
|
||||||
} catch (error) {
|
export function convertEnv(envOptions) {
|
||||||
val = ''
|
const result = {}
|
||||||
}
|
if (!envOptions) return result
|
||||||
}
|
|
||||||
ret[key] = val
|
for (const envKey in envOptions) {
|
||||||
if (typeof key === 'string') {
|
let envVal = envOptions[envKey]
|
||||||
process.env[key] = val
|
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
|
||||||
} else if (typeof key === 'object') {
|
|
||||||
process.env[key] = JSON.stringify(val)
|
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
|
||||||
}
|
|
||||||
|
result[envKey] = envVal
|
||||||
}
|
}
|
||||||
return ret
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +50,7 @@ function getConfFiles() {
|
|||||||
return ['.env', '.env.local', '.env.production']
|
return ['.env', '.env.local', '.env.production']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
|
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
|
||||||
let envConfig = {}
|
let envConfig = {}
|
||||||
confFiles.forEach((item) => {
|
confFiles.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
@@ -65,7 +70,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
|
|||||||
})
|
})
|
||||||
return envConfig
|
return envConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRootPath(...dir) {
|
|
||||||
return path.resolve(process.cwd(), ...dir)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import html from 'vite-plugin-html'
|
|
||||||
import { version } from '../../../package.json'
|
|
||||||
import { GLOB_CONFIG_FILE_NAME } from '../../constant'
|
|
||||||
|
|
||||||
export function configHtmlPlugin(viteEnv, isBuild) {
|
|
||||||
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
|
|
||||||
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`
|
|
||||||
|
|
||||||
const getAppConfigSrc = () => {
|
|
||||||
return `${path}${GLOB_CONFIG_FILE_NAME}?v=${version}-${new Date().getTime()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const htmlPlugin = html({
|
|
||||||
minify: isBuild,
|
|
||||||
inject: {
|
|
||||||
data: {
|
|
||||||
title: VITE_APP_TITLE,
|
|
||||||
},
|
|
||||||
tags: isBuild
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
tag: 'script',
|
|
||||||
attrs: {
|
|
||||||
src: getAppConfigSrc(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return htmlPlugin
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
|
||||||
import { unocss } from './unocss'
|
|
||||||
import { configHtmlPlugin } from './html'
|
|
||||||
import { configMockPlugin } from './mock'
|
|
||||||
|
|
||||||
export function createVitePlugins(viteEnv, isBuild) {
|
|
||||||
const plugins = [vue(), VueSetupExtend(), unocss(), configHtmlPlugin(viteEnv, isBuild)]
|
|
||||||
|
|
||||||
viteEnv?.VITE_APP_USE_MOCK && plugins.push(configMockPlugin(isBuild))
|
|
||||||
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import Unocss from 'unocss/vite'
|
|
||||||
import { presetUno, presetAttributify, presetIcons } from 'unocss'
|
|
||||||
|
|
||||||
// https://github.com/antfu/unocss
|
|
||||||
export function unocss() {
|
|
||||||
return Unocss({
|
|
||||||
presets: [presetUno(), presetAttributify(), presetIcons()],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const httpsRE = /^https:\/\//
|
|
||||||
export function createProxy(list = []) {
|
|
||||||
const ret = {}
|
|
||||||
for (const [prefix, target] of list) {
|
|
||||||
const isHttps = httpsRE.test(target)
|
|
||||||
|
|
||||||
// https://github.com/http-party/node-http-proxy#options
|
|
||||||
ret[prefix] = {
|
|
||||||
target: target,
|
|
||||||
changeOrigin: true,
|
|
||||||
ws: true,
|
|
||||||
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
|
|
||||||
// https is require secure=false
|
|
||||||
...(isHttps ? { secure: false } : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
26
commitlint.config.js
Normal file
26
commitlint.config.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
ignores: [(commit) => commit.includes('init')],
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
rules: {
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat',
|
||||||
|
'fix',
|
||||||
|
'docs',
|
||||||
|
'style',
|
||||||
|
'refactor',
|
||||||
|
'perf',
|
||||||
|
'test',
|
||||||
|
'build',
|
||||||
|
'ci',
|
||||||
|
'chore',
|
||||||
|
'revert',
|
||||||
|
'wip',
|
||||||
|
'mod',
|
||||||
|
'release',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
47
index.html
47
index.html
@@ -1,22 +1,35 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cn">
|
<html lang="cn">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Cache-control" content="no-cache" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<link rel="stylesheet" href="/resource/loading.css" />
|
||||||
|
|
||||||
<head>
|
<title><%= title %></title>
|
||||||
<meta charset="UTF-8" />
|
</head>
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Cache-control" content="no-cache" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<title>
|
|
||||||
<%= title %>
|
|
||||||
</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 白屏时的loading效果 -->
|
||||||
|
<div class="loading-container">
|
||||||
|
<div id="loadingLogo" class="loading-svg"></div>
|
||||||
|
<div class="loading-spin__container">
|
||||||
|
<div class="loading-spin">
|
||||||
|
<div class="left-0 top-0 loading-spin-item"></div>
|
||||||
|
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
|
||||||
|
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
|
||||||
|
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-title"><%= title %></div>
|
||||||
|
</div>
|
||||||
|
<script src="/resource/loading.js"></script>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"~/*": ["./*"],
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
},
|
||||||
|
"jsx": "preserve"
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
|
||||||
|
|
||||||
const modules = import.meta.globEager('./**/*.js')
|
|
||||||
const mockModules = []
|
|
||||||
Object.keys(modules).forEach((key) => {
|
|
||||||
if (key.includes('/_')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mockModules.push(...modules[key].default)
|
|
||||||
})
|
|
||||||
|
|
||||||
export function setupProdMockServer() {
|
|
||||||
createProdMockServer(mockModules)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { resolveToken } from '../_utils'
|
import { resolveToken } from '../utils'
|
||||||
|
|
||||||
const token = {
|
const token = {
|
||||||
admin: 'admin',
|
admin: 'admin',
|
||||||
5
mock/api/index.js
Normal file
5
mock/api/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import auth from './auth'
|
||||||
|
import user from './user'
|
||||||
|
import post from './post'
|
||||||
|
|
||||||
|
export default [...auth, ...user, ...post]
|
||||||
75
mock/api/post.js
Normal file
75
mock/api/post.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
url: '/api/posts',
|
||||||
|
method: 'get',
|
||||||
|
response: () => {
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
message: 'ok',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 36,
|
||||||
|
title: '使用纯css优雅配置移动端rem布局',
|
||||||
|
author: 'Ronnie',
|
||||||
|
category: '移动端,Css',
|
||||||
|
description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...',
|
||||||
|
content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样',
|
||||||
|
isRecommend: true,
|
||||||
|
isPublish: true,
|
||||||
|
createDate: '2021-11-04T04:03:36.000Z',
|
||||||
|
updateDate: '2021-11-04T04:03:36.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 35,
|
||||||
|
title: 'Vue2&Vue3项目风格指南',
|
||||||
|
author: 'Ronnie',
|
||||||
|
category: 'Vue',
|
||||||
|
description: '总结的Vue2和Vue3的项目风格',
|
||||||
|
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
|
||||||
|
isRecommend: true,
|
||||||
|
isPublish: true,
|
||||||
|
createDate: '2021-10-25T08:57:47.000Z',
|
||||||
|
updateDate: '2022-02-28T04:02:39.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 28,
|
||||||
|
title: '如何优雅的给图片添加水印',
|
||||||
|
author: '大脸怪',
|
||||||
|
category: 'JavaScript',
|
||||||
|
description: '优雅的给图片添加水印',
|
||||||
|
content: '我之前写过一篇文章记录了一次上传图片的优化史',
|
||||||
|
isRecommend: true,
|
||||||
|
isPublish: true,
|
||||||
|
createDate: '2021-06-24T18:46:19.000Z',
|
||||||
|
updateDate: '2021-09-23T07:51:22.000Z',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 26,
|
||||||
|
title: '前端缓存的理解',
|
||||||
|
author: '大脸怪',
|
||||||
|
category: 'Http',
|
||||||
|
description: '谈谈前端缓存的理解',
|
||||||
|
content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
|
||||||
|
isRecommend: true,
|
||||||
|
isPublish: true,
|
||||||
|
createDate: '2021-06-10T18:51:19.000Z',
|
||||||
|
updateDate: '2021-09-17T09:33:24.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
title: 'Promise的五个静态方法',
|
||||||
|
author: '大脸怪',
|
||||||
|
category: 'JavaScript',
|
||||||
|
description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景',
|
||||||
|
content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。',
|
||||||
|
isRecommend: true,
|
||||||
|
isPublish: true,
|
||||||
|
createDate: '2021-02-22T22:37:06.000Z',
|
||||||
|
updateDate: '2021-09-17T09:33:24.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { resolveToken } from '../_utils'
|
import { resolveToken } from '../utils'
|
||||||
|
|
||||||
const users = {
|
const users = {
|
||||||
admin: {
|
admin: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '大脸怪(admin)',
|
name: '大脸怪(admin)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['admin'],
|
role: ['admin'],
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: '大脸怪(editor)',
|
name: '大脸怪(editor)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
email: 'Ronnie@123.com',
|
email: 'Ronnie@123.com',
|
||||||
role: ['editor'],
|
role: ['editor'],
|
||||||
},
|
},
|
||||||
guest: {
|
guest: {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: '访客(guest)',
|
name: '访客(guest)',
|
||||||
avatar: 'https://gitee.com/zclzone/res/raw/master/qs-zone/blob/img/lADPDiQ3QDTwsz3NAarNAaw_428_426.jpg',
|
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||||
role: [],
|
role: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
6
mock/index.js
Normal file
6
mock/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||||
|
import api from './api'
|
||||||
|
|
||||||
|
export function setupProdMockServer() {
|
||||||
|
createProdMockServer(api)
|
||||||
|
}
|
||||||
2955
package-lock.json
generated
2955
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -1,47 +1,72 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-naive-admin",
|
"name": "vue-naive-admin",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"lint": "eslint --ext .js,.vue .",
|
"lint": "eslint --ext .js,.vue .",
|
||||||
"lint:fix": "eslint --fix --ext .js,.vue .",
|
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||||
"build": "vite build && esno ./build/script",
|
"lint:staged": "lint-staged",
|
||||||
"build:test": "vite build --mode test && esno ./build/script",
|
"build": "vite build",
|
||||||
"build:staging": "vite build --mode staging && esno ./build/script",
|
"build:test": "vite build --mode test",
|
||||||
"preview": "vite preview"
|
"build:github": "vite build --mode github && esno ./build/script",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"cz": "cz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/fa": "^0.11.0",
|
"@vueuse/core": "^8.4.2",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.11.0",
|
||||||
"js-cookie": "^3.0.1",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"md-editor-v3": "^1.11.4",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"pinia": "^2.0.9",
|
"pinia": "^2.0.13",
|
||||||
"vue": "^3.2.6",
|
"vue": "^3.2.31",
|
||||||
"vue-router": "^4.0.12"
|
"vue-router": "^4.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@unocss/preset-attributify": "^0.16.3",
|
"@commitlint/cli": "^17.0.3",
|
||||||
"@unocss/preset-icons": "^0.16.3",
|
"@commitlint/config-conventional": "^17.0.3",
|
||||||
"@unocss/preset-uno": "^0.16.3",
|
"@iconify/json": "^2.1.99",
|
||||||
"@vitejs/plugin-vue": "^1.6.0",
|
"@iconify/vue": "^3.2.1",
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
"@vitejs/plugin-vue": "^1.10.2",
|
||||||
"chalk": "^5.0.0",
|
"@vue/compiler-sfc": "^3.2.31",
|
||||||
|
"chalk": "^5.0.1",
|
||||||
|
"commitizen": "^4.2.4",
|
||||||
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
|
"cz-customizable": "^6.9.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.6.0",
|
"eslint": "^8.12.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^8.2.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"esno": "^0.13.0",
|
"esno": "^0.13.0",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.1",
|
||||||
"naive-ui": "^2.19.1",
|
"husky": "^8.0.1",
|
||||||
"prettier": "^2.5.1",
|
"lint-staged": "^13.0.3",
|
||||||
"sass": "^1.38.1",
|
"naive-ui": "^2.32.1",
|
||||||
"unocss": "^0.16.3",
|
"prettier": "^2.6.1",
|
||||||
"vite": "^2.7.6",
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"vite-plugin-html": "^2.1.1",
|
"sass": "^1.49.10",
|
||||||
|
"unocss": "^0.43.2",
|
||||||
|
"unplugin-auto-import": "^0.9.2",
|
||||||
|
"unplugin-icons": "^0.14.1",
|
||||||
|
"unplugin-vue-components": "^0.17.21",
|
||||||
|
"vite": "^3.0.9",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
"vite-plugin-vue-setup-extend": "^0.3.0"
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vite-plugin-vue-setup-extend-plus": "^0.1.0"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "node_modules/cz-customizable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,vue}": [
|
||||||
|
"eslint --ext .js,.vue ."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5734
pnpm-lock.yaml
generated
Normal file
5734
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" fill="#316c72"></path></svg>
|
||||||
|
After Width: | Height: | Size: 825 B |
91
public/resource/loading.css
Normal file
91
public/resource/loading.css
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
.loading-container {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-svg {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin__container {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 36px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
animation: loadingSpin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right-0 {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.top-0 {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spin-item {
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingSpin {
|
||||||
|
from {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-delay-500 {
|
||||||
|
-webkit-animation-delay: 500ms;
|
||||||
|
animation-delay: 500ms;
|
||||||
|
}
|
||||||
|
.loading-delay-1000 {
|
||||||
|
-webkit-animation-delay: 1000ms;
|
||||||
|
animation-delay: 1000ms;
|
||||||
|
}
|
||||||
|
.loading-delay-1500 {
|
||||||
|
-webkit-animation-delay: 1500ms;
|
||||||
|
animation-delay: 1500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6a6a6a;
|
||||||
|
}
|
||||||
25
public/resource/loading.js
Normal file
25
public/resource/loading.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 初始化加载效果的svg格式logo
|
||||||
|
* @param {string} id - 元素id
|
||||||
|
*/
|
||||||
|
function initSvgLogo(id) {
|
||||||
|
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" style="fill:currentColor"></path></svg>`
|
||||||
|
const appEl = document.querySelector(id)
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = svgStr
|
||||||
|
if (appEl) {
|
||||||
|
appEl.appendChild(div)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addThemeColorCssVars() {
|
||||||
|
const key = '__THEME_COLOR__'
|
||||||
|
const defaultColor = '#316c72'
|
||||||
|
const themeColor = window.localStorage.getItem(key) || defaultColor
|
||||||
|
const cssVars = `--primary-color: ${themeColor}`
|
||||||
|
document.documentElement.style.cssText = cssVars
|
||||||
|
}
|
||||||
|
|
||||||
|
addThemeColorCssVars()
|
||||||
|
|
||||||
|
initSvgLogo('#loadingLogo')
|
||||||
2
settings/index.js
Normal file
2
settings/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './theme.json'
|
||||||
|
export * from './proxy-config'
|
||||||
18
settings/proxy-config.js
Normal file
18
settings/proxy-config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const proxyConfigMappings = {
|
||||||
|
dev: {
|
||||||
|
prefix: '/api',
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
prefix: '/api',
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
prefix: '/api',
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProxyConfig(envType = 'dev') {
|
||||||
|
return proxyConfigMappings[envType]
|
||||||
|
}
|
||||||
17
settings/theme.json
Normal file
17
settings/theme.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"height": 60
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"visible": true,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"naiveThemeOverrides": {
|
||||||
|
"common": {
|
||||||
|
"primaryColor": "#316C72FF",
|
||||||
|
"primaryColorHover": "#316C72E3",
|
||||||
|
"primaryColorPressed": "#2B4C59FF",
|
||||||
|
"primaryColorSuppl": "#316C7263"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/App.vue
30
src/App.vue
@@ -1,25 +1,11 @@
|
|||||||
<script setup>
|
|
||||||
import AppProvider from '@/components/AppProvider/index.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view>
|
<AppProvider>
|
||||||
<template #default="{ Component, route }">
|
<router-view v-slot="{ Component }">
|
||||||
<app-provider>
|
<component :is="Component" />
|
||||||
<keep-alive v-if="route.meta && route.meta.keepAlive">
|
</router-view>
|
||||||
<component :is="Component" :key="route.fullPath" />
|
</AppProvider>
|
||||||
</keep-alive>
|
|
||||||
<component :is="Component" v-else :key="route.fullPath" />
|
|
||||||
</app-provider>
|
|
||||||
</template>
|
|
||||||
</router-view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<script setup>
|
||||||
#app {
|
import AppProvider from '@/components/common/AppProvider.vue'
|
||||||
height: 100%;
|
</script>
|
||||||
.n-config-provider {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defAxios } from '@/utils/http'
|
|
||||||
|
|
||||||
export const login = (data) => {
|
|
||||||
return defAxios({
|
|
||||||
url: '/auth/login',
|
|
||||||
method: 'post',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const refreshToken = () => {
|
|
||||||
return defAxios({
|
|
||||||
url: '/auth/refreshToken',
|
|
||||||
method: 'post',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
6
src/api/index.js
Normal file
6
src/api/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getUser: () => request.get('/user'),
|
||||||
|
refreshToken: () => request.post('/auth/refreshToken'),
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { defAxios } from '@/utils/http'
|
|
||||||
|
|
||||||
export function getUsers(data = {}) {
|
|
||||||
return defAxios({
|
|
||||||
url: '/users',
|
|
||||||
method: 'get',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUser(id) {
|
|
||||||
if (id) {
|
|
||||||
return defAxios({
|
|
||||||
url: `/user/${id}`,
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return defAxios({
|
|
||||||
url: '/user',
|
|
||||||
method: 'get',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveUser(data = {}, id) {
|
|
||||||
if (id) {
|
|
||||||
return defAxios({
|
|
||||||
url: '/user',
|
|
||||||
method: 'put',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return defAxios({
|
|
||||||
url: `/user/${id}`,
|
|
||||||
method: 'put',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
BIN
src/assets/images/404.webp
Normal file
BIN
src/assets/images/404.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/login_banner.webp
Normal file
BIN
src/assets/images/login_banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/login_bg.webp
Normal file
BIN
src/assets/images/login_bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
1
src/assets/svg/logo.svg
Normal file
1
src/assets/svg/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 811 B |
@@ -1,52 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { isNullOrUndef } from '@/utils/is'
|
|
||||||
import { useDialog } from 'naive-ui'
|
|
||||||
|
|
||||||
const NDialog = useDialog()
|
|
||||||
|
|
||||||
class Dialog {
|
|
||||||
success(title, option) {
|
|
||||||
this.showDialog('success', { title, ...option })
|
|
||||||
}
|
|
||||||
|
|
||||||
warning(title, option) {
|
|
||||||
this.showDialog('warning', { title, ...option })
|
|
||||||
}
|
|
||||||
|
|
||||||
error(title, option) {
|
|
||||||
this.showDialog('error', { title, ...option })
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(type = 'success', option) {
|
|
||||||
if (isNullOrUndef(option.title)) {
|
|
||||||
// ! 没有title的情况
|
|
||||||
option.showIcon = false
|
|
||||||
}
|
|
||||||
NDialog[type]({
|
|
||||||
positiveText: 'OK',
|
|
||||||
closable: false,
|
|
||||||
...option,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
confirm(option = {}) {
|
|
||||||
this.showDialog(option.type || 'error', {
|
|
||||||
positiveText: '确定',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: option.confirm,
|
|
||||||
onNegativeClick: option.cancel,
|
|
||||||
onMaskClick: option.cancel,
|
|
||||||
...option,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window['$dialog'] = new Dialog()
|
|
||||||
Object.freeze(window.$dialog)
|
|
||||||
Object.defineProperty(window, '$dialog', {
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template></template>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useLoadingBar } from 'naive-ui'
|
|
||||||
window['$loadingBar'] = useLoadingBar()
|
|
||||||
Object.defineProperty(window, '$loadingBar', {
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template></template>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
const NMessage = useMessage()
|
|
||||||
|
|
||||||
let loadingMessage = null
|
|
||||||
|
|
||||||
class Message {
|
|
||||||
/**
|
|
||||||
* 规则:
|
|
||||||
* * 同一Message实例只显示一个loading message,如果需要显示多个可以创建多个Message实例
|
|
||||||
* * 新的message会替换正在显示的loading message
|
|
||||||
* * 默认已创建一个Message实例$message挂载到window,同时也将Message类挂载到了window
|
|
||||||
*/
|
|
||||||
|
|
||||||
removeMessage(message, duration = 2000) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (message) {
|
|
||||||
message.destroy()
|
|
||||||
message = null
|
|
||||||
}
|
|
||||||
}, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage(type, content, option = {}) {
|
|
||||||
if (this.loadingMessage && this.loadingMessage.type === 'loading') {
|
|
||||||
// 如果存在则替换正在显示的loading message
|
|
||||||
this.loadingMessage.type = type
|
|
||||||
this.loadingMessage.content = content
|
|
||||||
|
|
||||||
if (type !== 'loading') {
|
|
||||||
// 非loading message需设置自动清除
|
|
||||||
this.removeMessage(this.loadingMessage, option.duration)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 不存在正在显示的loading则新建一个message,如果新建的message是loading message则将message赋值存储下来
|
|
||||||
let message = NMessage[type](content, option)
|
|
||||||
if (type === 'loading') {
|
|
||||||
this.loadingMessage = message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loading(content) {
|
|
||||||
this.showMessage('loading', content, { duration: 0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
success(content, option = {}) {
|
|
||||||
this.showMessage('success', content, option)
|
|
||||||
}
|
|
||||||
|
|
||||||
error(content, option = {}) {
|
|
||||||
this.showMessage('error', content, option)
|
|
||||||
}
|
|
||||||
|
|
||||||
info(content, option = {}) {
|
|
||||||
this.showMessage('info', content, option)
|
|
||||||
}
|
|
||||||
|
|
||||||
warning(content, option = {}) {
|
|
||||||
this.showMessage('warning', content, option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window['$message'] = new Message()
|
|
||||||
|
|
||||||
Object.defineProperty(window, '$message', {
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template></template>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { NConfigProvider, NGlobalStyle, NLoadingBarProvider, NMessageProvider, NDialogProvider } from 'naive-ui'
|
|
||||||
|
|
||||||
import MessageContent from './MessageContent.vue'
|
|
||||||
import DialogContent from './DialogContent.vue'
|
|
||||||
import LoadingBar from './LoadingBar.vue'
|
|
||||||
|
|
||||||
import { useAppStore } from '@/store/modules/app'
|
|
||||||
const appStore = useAppStore()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-config-provider :theme-overrides="appStore.themeOverrides">
|
|
||||||
<n-global-style />
|
|
||||||
<n-loading-bar-provider>
|
|
||||||
<loading-bar />
|
|
||||||
<n-dialog-provider>
|
|
||||||
<dialog-content />
|
|
||||||
<n-message-provider>
|
|
||||||
<message-content />
|
|
||||||
<slot name="default"></slot>
|
|
||||||
</n-message-provider>
|
|
||||||
</n-dialog-provider>
|
|
||||||
</n-loading-bar-provider>
|
|
||||||
</n-config-provider>
|
|
||||||
</template>
|
|
||||||
13
src/components/common/AppFooter.vue
Normal file
13
src/components/common/AppFooter.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<footer text-14 f-c-c flex-col color="#6a6a6a">
|
||||||
|
<p>
|
||||||
|
Copyright©2022
|
||||||
|
<a href="https://github.com/zclzone" target="__blank" hover="decoration-underline color-primary"> 大脸怪</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="http://beian.miit.gov.cn/" target="__blank" hover="decoration-underline color-primary">
|
||||||
|
赣ICP备2020015008号-1
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
50
src/components/common/AppProvider.vue
Normal file
50
src/components/common/AppProvider.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<n-config-provider wh-full :theme-overrides="naiveThemeOverrides">
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<slot></slot>
|
||||||
|
<NaiveProviderContent />
|
||||||
|
</n-message-provider>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
|
||||||
|
import { useCssVar } from '@vueuse/core'
|
||||||
|
import { kebabCase } from 'lodash-es'
|
||||||
|
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
|
||||||
|
import { naiveThemeOverrides } from '~/settings'
|
||||||
|
|
||||||
|
function setupCssVar() {
|
||||||
|
const common = naiveThemeOverrides.common
|
||||||
|
for (const key in common) {
|
||||||
|
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
|
||||||
|
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载naive组件的方法至window, 以便在全局使用
|
||||||
|
function setupNaiveTools() {
|
||||||
|
window.$loadingBar = useLoadingBar()
|
||||||
|
window.$notification = useNotification()
|
||||||
|
|
||||||
|
window.$message = setupMessage(useMessage())
|
||||||
|
window.$dialog = setupDialog(useDialog())
|
||||||
|
}
|
||||||
|
|
||||||
|
const NaiveProviderContent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
setupCssVar()
|
||||||
|
setupNaiveTools()
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
140
src/components/common/ScrollX.vue
Normal file
140
src/components/common/ScrollX.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||||
|
<template v-if="showArrow && isOverflow">
|
||||||
|
<div class="left" @click="handleMouseWheel({ wheelDelta: 120 })">
|
||||||
|
<icon-ic:baseline-keyboard-arrow-left />
|
||||||
|
</div>
|
||||||
|
<div class="right" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||||
|
<icon-ic:baseline-keyboard-arrow-right />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="content"
|
||||||
|
class="content"
|
||||||
|
:class="{ overflow: isOverflow && showArrow }"
|
||||||
|
:style="{
|
||||||
|
transform: `translateX(${translateX}px)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { debounce } from '@/utils'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
showArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const translateX = ref(0)
|
||||||
|
const content = ref(null)
|
||||||
|
const wrapper = ref(null)
|
||||||
|
const isOverflow = ref(false)
|
||||||
|
|
||||||
|
const refreshIsOverflow = debounce(() => {
|
||||||
|
const wrapperWidth = wrapper.value.offsetWidth
|
||||||
|
const contentWidth = content.value.offsetWidth
|
||||||
|
isOverflow.value = contentWidth > wrapperWidth
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
function handleMouseWheel(e) {
|
||||||
|
const { wheelDelta } = e
|
||||||
|
const wrapperWidth = wrapper.value.offsetWidth
|
||||||
|
const contentWidth = content.value.offsetWidth
|
||||||
|
/**
|
||||||
|
* @wheelDelta 平行滚动的值 >0: 右移 <0: 左移
|
||||||
|
* @translateX 内容translateX的值
|
||||||
|
* @wrapperWidth 容器的宽度
|
||||||
|
* @contentWidth 内容的宽度
|
||||||
|
*/
|
||||||
|
if (wheelDelta < 0) {
|
||||||
|
if (wrapperWidth > contentWidth && translateX.value < -10) return
|
||||||
|
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
|
||||||
|
}
|
||||||
|
if (wheelDelta > 0 && translateX.value > 10) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
translateX.value += wheelDelta
|
||||||
|
resetTranslateX(wrapperWidth, contentWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||||
|
if (!isOverflow.value) {
|
||||||
|
translateX.value = 0
|
||||||
|
} else if (-translateX.value > contentWidth - wrapperWidth) {
|
||||||
|
translateX.value = wrapperWidth - contentWidth
|
||||||
|
} else if (translateX.value > 0) {
|
||||||
|
translateX.value = 0
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
const observer = new MutationObserver(refreshIsOverflow)
|
||||||
|
onMounted(() => {
|
||||||
|
refreshIsOverflow()
|
||||||
|
|
||||||
|
window.addEventListener('resize', refreshIsOverflow)
|
||||||
|
// 监听内容宽度刷新是否超出
|
||||||
|
observer.observe(content.value, { childList: true })
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', refreshIsOverflow)
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
z-index: 9;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
.content {
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
transition: transform 0.5s;
|
||||||
|
&.overflow {
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid #e0e0e6;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
src/components/custom/CustomIcon.vue
Normal file
25
src/components/custom/CustomIcon.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { renderCustomIcon } from '@/utils/icon'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** 图标名称(图片的文件名) */
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 14,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconCom = computed(() => renderCustomIcon(props.icon, props))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="iconCom" />
|
||||||
|
</template>
|
||||||
24
src/components/custom/SvgIcon.vue
Normal file
24
src/components/custom/SvgIcon.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup name="SvgIcon">
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
prefix: {
|
||||||
|
type: String,
|
||||||
|
default: 'icon-custom',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'currentColor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg aria-hidden="true" width="1em" height="1em">
|
||||||
|
<use :xlink:href="symbolId" :fill="color" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view>
|
<router-view v-slot="{ Component, route }">
|
||||||
<template #default="{ Component, route }">
|
<transition name="fade-slide" mode="out-in" appear>
|
||||||
<transition name="fade-slide" mode="out-in" appear>
|
<keep-alive :include="keepAliveRouteNames">
|
||||||
<component :is="Component" :key="route.fullPath"></component>
|
<component :is="Component" v-if="appStore.reloadFlag" :key="route.path" />
|
||||||
</transition>
|
</keep-alive>
|
||||||
</template>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const allRoutes = router.getRoutes()
|
||||||
|
const keepAliveRouteNames = computed(() => {
|
||||||
|
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
const router = useRouter()
|
|
||||||
const { currentRoute } = router
|
|
||||||
|
|
||||||
function handleBreadClick(path) {
|
|
||||||
if (path === currentRoute.value.path) return
|
|
||||||
router.push(path)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-breadcrumb>
|
|
||||||
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
|
||||||
{{ item.meta.title }}
|
|
||||||
</n-breadcrumb-item>
|
|
||||||
</n-breadcrumb>
|
|
||||||
</template>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { NDropdown } from 'naive-ui'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: '退出登录',
|
|
||||||
key: 'logout',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function handleSelect(key) {
|
|
||||||
if (key === 'logout') {
|
|
||||||
userStore.logout()
|
|
||||||
$message.success('已退出登录')
|
|
||||||
router.push({ path: '/login' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-dropdown :options="options" @select="handleSelect">
|
|
||||||
<div class="avatar">
|
|
||||||
<img :src="userStore.avatar" />
|
|
||||||
<span>{{ userStore.name }}</span>
|
|
||||||
</div>
|
|
||||||
</n-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.avatar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
30
src/layout/components/header/components/BreadCrumb.vue
Normal file
30
src/layout/components/header/components/BreadCrumb.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<n-breadcrumb>
|
||||||
|
<n-breadcrumb-item
|
||||||
|
v-for="item in route.matched.filter((item) => !!item.meta?.title)"
|
||||||
|
:key="item.path"
|
||||||
|
@click="handleBreadClick(item.path)"
|
||||||
|
>
|
||||||
|
<component :is="getIcon(item.meta)" />
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
</n-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function handleBreadClick(path) {
|
||||||
|
if (path === route.path) return
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(meta) {
|
||||||
|
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||||
|
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
12
src/layout/components/header/components/FullScreen.vue
Normal file
12
src/layout/components/header/components/FullScreen.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
|
||||||
|
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
|
||||||
|
<icon-ant-design:fullscreen-outlined v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
|
||||||
|
const { isFullscreen, toggle } = useFullscreen()
|
||||||
|
</script>
|
||||||
11
src/layout/components/header/components/GithubSite.vue
Normal file
11
src/layout/components/header/components/GithubSite.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||||
|
<icon-mdi:github />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function handleLinkClick() {
|
||||||
|
window.open('https://github.com/zclzone/vue-naive-admin')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
12
src/layout/components/header/components/MenuCollapse.vue
Normal file
12
src/layout/components/header/components/MenuCollapse.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<n-icon size="20" cursor-pointer @click="appStore.switchCollapsed">
|
||||||
|
<icon-mdi:format-indent-increase v-if="appStore.collapsed" />
|
||||||
|
<icon-mdi:format-indent-decrease v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
37
src/layout/components/header/components/UserAvatar.vue
Normal file
37
src/layout/components/header/components/UserAvatar.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<n-dropdown :options="options" @select="handleSelect">
|
||||||
|
<div flex items-center cursor-pointer>
|
||||||
|
<img :src="userStore.avatar" mr10 w-35 h-35 rounded-full />
|
||||||
|
<span>{{ userStore.name }}</span>
|
||||||
|
</div>
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { renderIcon } from '@/utils/icon'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout',
|
||||||
|
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function handleSelect(key) {
|
||||||
|
if (key === 'logout') {
|
||||||
|
$dialog.confirm({
|
||||||
|
title: '提示',
|
||||||
|
type: 'info',
|
||||||
|
content: '确认退出?',
|
||||||
|
confirm() {
|
||||||
|
userStore.logout()
|
||||||
|
$message.success('已退出登录')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
<script setup>
|
|
||||||
import BreadCrumb from './BreadCrumb.vue'
|
|
||||||
import HeaderAction from './HeaderAction.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="header">
|
<header px-15 h-full flex items-center>
|
||||||
<bread-crumb />
|
<div flex items-center>
|
||||||
<header-action />
|
<MenuCollapse />
|
||||||
|
<BreadCrumb ml-15 />
|
||||||
|
</div>
|
||||||
|
<div ml-auto flex items-center>
|
||||||
|
<GithubSite />
|
||||||
|
<FullScreen />
|
||||||
|
<UserAvatar />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<script setup>
|
||||||
.header {
|
import BreadCrumb from './components/BreadCrumb.vue'
|
||||||
padding: 0 35px;
|
import MenuCollapse from './components/MenuCollapse.vue'
|
||||||
height: 100%;
|
import FullScreen from './components/FullScreen.vue'
|
||||||
display: flex;
|
import UserAvatar from './components/UserAvatar.vue'
|
||||||
justify-content: space-between;
|
import GithubSite from './components/GithubSite.vue'
|
||||||
align-items: center;
|
</script>
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { NGradientText, NIcon } from 'naive-ui'
|
|
||||||
import { LastfmSquare } from '@vicons/fa'
|
|
||||||
const title = import.meta.env.VITE_APP_TITLE
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="logo">
|
|
||||||
<n-icon size="36" color="#316c72">
|
|
||||||
<lastfm-square />
|
|
||||||
</n-icon>
|
|
||||||
<router-link to="/">
|
|
||||||
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.logo {
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
a {
|
|
||||||
margin-left: 5px;
|
|
||||||
.n-gradient-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { NMenu } from 'naive-ui'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const permissionStore = usePermissionStore()
|
|
||||||
|
|
||||||
const { currentRoute } = router
|
|
||||||
const routes = permissionStore.routes
|
|
||||||
|
|
||||||
const menuOptions = computed(() => {
|
|
||||||
return generateOptions(routes, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
function resolvePath(...pathes) {
|
|
||||||
return (
|
|
||||||
'/' +
|
|
||||||
pathes
|
|
||||||
.filter((path) => !!path && path !== '/')
|
|
||||||
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
|
||||||
.join('/')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateOptions(routes, basePath) {
|
|
||||||
let options = []
|
|
||||||
routes.forEach((route) => {
|
|
||||||
if (route.name && !route.isHidden) {
|
|
||||||
let curOption = {
|
|
||||||
label: (route.meta && route.meta.title) || route.name,
|
|
||||||
key: route.name,
|
|
||||||
path: resolvePath(basePath, route.path),
|
|
||||||
}
|
|
||||||
if (route.children && route.children.length) {
|
|
||||||
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
|
|
||||||
}
|
|
||||||
if (curOption.children && curOption.children.length <= 1) {
|
|
||||||
if (curOption.children.length === 1) {
|
|
||||||
curOption = { ...curOption.children[0] }
|
|
||||||
}
|
|
||||||
delete curOption.children
|
|
||||||
}
|
|
||||||
options.push(curOption)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMenuSelect(key, item) {
|
|
||||||
router.push(item.path)
|
|
||||||
|
|
||||||
// 通过path重定向
|
|
||||||
// router.push({
|
|
||||||
// path: '/redirect',
|
|
||||||
// query: { redirect: item.path },
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n-menu
|
|
||||||
class="side-menu"
|
|
||||||
:root-indent="20"
|
|
||||||
:options="menuOptions"
|
|
||||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
|
||||||
@update:value="handleMenuSelect"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.n-menu {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-left: 10px;
|
|
||||||
.n-menu-item {
|
|
||||||
margin-top: 0;
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.n-menu-item--selected {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-right: 3px solid $primaryColor;
|
|
||||||
background-color: #16243a;
|
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-menu-item-content-header {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-submenu-children {
|
|
||||||
.n-menu-item-content-header {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
position: relative;
|
|
||||||
overflow: visible !important;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -15px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// .side-menu {
|
|
||||||
// // padding-left: 15px;
|
|
||||||
// .n-menu-item-content-header {
|
|
||||||
// color: #fff !important;
|
|
||||||
// font-weight: bold;
|
|
||||||
// font-size: 14px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .n-submenu {
|
|
||||||
// .n-menu-item-content-header {
|
|
||||||
// color: #fff !important;
|
|
||||||
// font-weight: bold;
|
|
||||||
// font-size: 14px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .n-submenu-children {
|
|
||||||
// .n-menu-item-content-header {
|
|
||||||
// color: #fff !important;
|
|
||||||
// font-weight: normal;
|
|
||||||
// font-size: 12px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .n-menu-item {
|
|
||||||
// border-top-left-radius: 5px;
|
|
||||||
// border-bottom-left-radius: 5px;
|
|
||||||
// &:hover,
|
|
||||||
// &.n-menu-item--selected::before {
|
|
||||||
// background-color: #16243a;
|
|
||||||
// right: 0;
|
|
||||||
// left: 0;
|
|
||||||
// border-right: 3px solid $primaryColor;
|
|
||||||
// background-color: unset !important;
|
|
||||||
// background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
</style>
|
|
||||||
15
src/layout/components/sidebar/components/SideLogo.vue
Normal file
15
src/layout/components/sidebar/components/SideLogo.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<router-link h-60 f-c-c to="/">
|
||||||
|
<icon-custom-logo text-36 color-primary></icon-custom-logo>
|
||||||
|
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0>
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
const title = import.meta.env.VITE_TITLE
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
114
src/layout/components/sidebar/components/SideMenu.vue
Normal file
114
src/layout/components/sidebar/components/SideMenu.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<n-menu
|
||||||
|
class="side-menu"
|
||||||
|
accordion
|
||||||
|
:indent="18"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:options="menuOptions"
|
||||||
|
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||||
|
@update:value="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
|
|
||||||
|
import { isExternal } from '@/utils/is'
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
import { renderCustomIcon, renderIcon } from '@/utils/icon'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { currentRoute } = router
|
||||||
|
|
||||||
|
const menuOptions = computed(() => {
|
||||||
|
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolvePath(basePath, path) {
|
||||||
|
if (isExternal(path)) return path
|
||||||
|
return (
|
||||||
|
'/' +
|
||||||
|
[basePath, path]
|
||||||
|
.filter((path) => !!path && path !== '/')
|
||||||
|
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
||||||
|
.join('/')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenuItem(route, basePath = '') {
|
||||||
|
let menuItem = {
|
||||||
|
label: (route.meta && route.meta.title) || route.name,
|
||||||
|
key: route.name,
|
||||||
|
path: resolvePath(basePath, route.path),
|
||||||
|
icon: getIcon(route.meta),
|
||||||
|
order: route.meta?.order || 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : []
|
||||||
|
|
||||||
|
if (!visibleChildren.length) return menuItem
|
||||||
|
|
||||||
|
if (visibleChildren.length === 1) {
|
||||||
|
// 单个子路由处理
|
||||||
|
const singleRoute = visibleChildren[0]
|
||||||
|
menuItem = {
|
||||||
|
label: singleRoute.meta?.title || singleRoute.name,
|
||||||
|
key: singleRoute.name,
|
||||||
|
path: resolvePath(menuItem.path, singleRoute.path),
|
||||||
|
icon: getIcon(singleRoute.meta),
|
||||||
|
order: menuItem.order,
|
||||||
|
}
|
||||||
|
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : []
|
||||||
|
|
||||||
|
if (visibleItems.length === 1) {
|
||||||
|
menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||||
|
} else if (visibleItems.length > 1) {
|
||||||
|
menuItem.children = visibleItems.map((item) => getMenuItem(item, menuItem.path)).sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
menuItem.children = visibleChildren
|
||||||
|
.map((item) => getMenuItem(item, menuItem.path))
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(meta) {
|
||||||
|
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||||
|
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenuSelect(key, item) {
|
||||||
|
if (isExternal(item.path)) {
|
||||||
|
window.open(item.path)
|
||||||
|
} else {
|
||||||
|
if (item.path === currentRoute.value.path && !currentRoute.value.meta?.keepAlive) {
|
||||||
|
appStore.reloadPage()
|
||||||
|
} else {
|
||||||
|
router.push(item.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.side-menu:not(.n-menu--collapsed) {
|
||||||
|
.n-menu-item-content {
|
||||||
|
&::before {
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
&.n-menu-item-content--selected,
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
border-left: 4px solid var(--primaryColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SideLogo from './SideLogo.vue'
|
import SideLogo from './components/SideLogo.vue'
|
||||||
import SideMenu from './SideMenu.vue'
|
import SideMenu from './components/SideMenu.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<side-logo />
|
<SideLogo />
|
||||||
<side-menu />
|
<SideMenu />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
117
src/layout/components/tags/ContextMenu.vue
Normal file
117
src/layout/components/tags/ContextMenu.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<n-dropdown
|
||||||
|
:show="show"
|
||||||
|
:options="options"
|
||||||
|
:x="x"
|
||||||
|
:y="y"
|
||||||
|
placement="bottom-start"
|
||||||
|
@clickoutside="handleHideDropdown"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useTagsStore } from '@/store/modules/tags'
|
||||||
|
import { renderIcon } from '@/utils/icon'
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
currentPath: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
|
||||||
|
const tagsStore = useTagsStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const options = computed(() => [
|
||||||
|
{
|
||||||
|
label: '重新加载',
|
||||||
|
key: 'reload',
|
||||||
|
disabled: props.currentPath !== tagsStore.activeTag,
|
||||||
|
icon: renderIcon('mdi:refresh', { size: '14px' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
key: 'close',
|
||||||
|
disabled: tagsStore.tags.length <= 1,
|
||||||
|
icon: renderIcon('mdi:close', { size: '14px' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭其他',
|
||||||
|
key: 'close-other',
|
||||||
|
disabled: tagsStore.tags.length <= 1,
|
||||||
|
icon: renderIcon('mdi:arrow-expand-horizontal', { size: '14px' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭左侧',
|
||||||
|
key: 'close-left',
|
||||||
|
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[0].path,
|
||||||
|
icon: renderIcon('mdi:arrow-expand-left', { size: '14px' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭右侧',
|
||||||
|
key: 'close-right',
|
||||||
|
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
|
||||||
|
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const actionMap = new Map([
|
||||||
|
[
|
||||||
|
'reload',
|
||||||
|
() => {
|
||||||
|
appStore.reloadPage()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'close',
|
||||||
|
() => {
|
||||||
|
tagsStore.removeTag(props.currentPath)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'close-other',
|
||||||
|
() => {
|
||||||
|
tagsStore.removeOther(props.currentPath)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'close-left',
|
||||||
|
() => {
|
||||||
|
tagsStore.removeLeft(props.currentPath)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'close-right',
|
||||||
|
() => {
|
||||||
|
tagsStore.removeRight(props.currentPath)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
function handleHideDropdown() {
|
||||||
|
emit('update:show', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(key) {
|
||||||
|
const actionFn = actionMap.get(key)
|
||||||
|
actionFn && actionFn()
|
||||||
|
handleHideDropdown()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
86
src/layout/components/tags/index.vue
Normal file
86
src/layout/components/tags/index.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<ScrollX>
|
||||||
|
<n-tag
|
||||||
|
v-for="tag in tagsStore.tags"
|
||||||
|
:key="tag.path"
|
||||||
|
class="px-15 mx-5 rounded-4 cursor-pointer hover:color-primary"
|
||||||
|
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
|
||||||
|
:closable="tagsStore.tags.length > 1"
|
||||||
|
@click="handleTagClick(tag.path)"
|
||||||
|
@close.stop="tagsStore.removeTag(tag.path)"
|
||||||
|
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||||
|
>
|
||||||
|
{{ tag.title }}
|
||||||
|
</n-tag>
|
||||||
|
<ContextMenu
|
||||||
|
v-if="contextMenuOption.show"
|
||||||
|
v-model:show="contextMenuOption.show"
|
||||||
|
:current-path="contextMenuOption.currentPath"
|
||||||
|
:x="contextMenuOption.x"
|
||||||
|
:y="contextMenuOption.y"
|
||||||
|
/>
|
||||||
|
</ScrollX>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="Tags">
|
||||||
|
import ContextMenu from './ContextMenu.vue'
|
||||||
|
import { useTagsStore } from '@/store/modules/tags'
|
||||||
|
import ScrollX from '@/components/common/ScrollX.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const tagsStore = useTagsStore()
|
||||||
|
|
||||||
|
const contextMenuOption = reactive({
|
||||||
|
show: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
currentPath: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
const { name, path } = route
|
||||||
|
const title = route.meta?.title
|
||||||
|
tagsStore.addTag({ name, path, title })
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTagClick = (path) => {
|
||||||
|
tagsStore.setActiveTag(path)
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContextMenu() {
|
||||||
|
contextMenuOption.show = true
|
||||||
|
}
|
||||||
|
function hideContextMenu() {
|
||||||
|
contextMenuOption.show = false
|
||||||
|
}
|
||||||
|
function setContextMenu(x, y, currentPath) {
|
||||||
|
Object.assign(contextMenuOption, { x, y, currentPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右击菜单
|
||||||
|
async function handleContextMenu(e, tagItem) {
|
||||||
|
const { clientX, clientY } = e
|
||||||
|
hideContextMenu()
|
||||||
|
setContextMenu(clientX, clientY, tagItem.path)
|
||||||
|
await nextTick()
|
||||||
|
showContextMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.n-tag__close {
|
||||||
|
box-sizing: content-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transform: translateX(5px);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,30 +1,38 @@
|
|||||||
<script setup>
|
|
||||||
import { NLayout, NLayoutHeader, NLayoutSider } from 'naive-ui'
|
|
||||||
|
|
||||||
import AppHeader from './components/header/index.vue'
|
|
||||||
import SideMenu from './components/sidebar/index.vue'
|
|
||||||
import AppMain from './components/AppMain.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<n-layout has-sider h-full>
|
||||||
<n-layout has-sider position="absolute">
|
<n-layout-sider
|
||||||
<n-layout-sider :width="200" :collapsed-width="0" :native-scrollbar="false">
|
bordered
|
||||||
<side-menu />
|
collapse-mode="width"
|
||||||
</n-layout-sider>
|
:collapsed-width="64"
|
||||||
<n-layout>
|
:width="220"
|
||||||
<n-layout-header style="height: 100px; background-color: #f5f6fb">
|
:native-scrollbar="false"
|
||||||
<app-header />
|
:collapsed="appStore.collapsed"
|
||||||
</n-layout-header>
|
>
|
||||||
<n-layout
|
<SideBar />
|
||||||
position="absolute"
|
</n-layout-sider>
|
||||||
content-style="padding: 0 35px 35px"
|
<n-layout>
|
||||||
style="top: 100px; background-color: #f5f6fb"
|
<n-layout-header bg-white border-b bc-eee :style="`height: ${header.height ?? 60}px`">
|
||||||
:native-scrollbar="false"
|
<AppHeader />
|
||||||
>
|
</n-layout-header>
|
||||||
<app-main />
|
|
||||||
</n-layout>
|
<n-layout bg="#f5f6fb" :style="`height: calc(100% - ${header.height ?? 60}px)`">
|
||||||
|
<AppTags v-if="tags.visible" :style="`height: ${tags.height ?? 50}px`" />
|
||||||
|
<AppMain
|
||||||
|
class="cus-scroll border-t bc-eee overflow-auto"
|
||||||
|
:style="{ height: `calc(100% - ${tags.visible ? tags.height ?? 50 : 0}px)` }"
|
||||||
|
/>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</div>
|
</n-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppHeader from './components/header/index.vue'
|
||||||
|
import SideBar from './components/sidebar/index.vue'
|
||||||
|
import AppMain from './components/AppMain.vue'
|
||||||
|
import AppTags from './components/tags/index.vue'
|
||||||
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
import { header, tags } from '~/settings'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
</script>
|
||||||
|
|||||||
12
src/main.js
12
src/main.js
@@ -1,19 +1,21 @@
|
|||||||
|
import '@/styles/reset.css'
|
||||||
import '@/styles/index.scss'
|
import '@/styles/index.scss'
|
||||||
import 'uno.css'
|
import 'uno.css'
|
||||||
|
import 'virtual:svg-icons-register'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
import { setupRouter } from '@/router'
|
import { setupRouter } from '@/router'
|
||||||
import { setupStore } from '@/store'
|
import { setupStore } from '@/store'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
async function bootstrap() {
|
function setupApp() {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
setupStore(app)
|
setupStore(app)
|
||||||
|
|
||||||
setupRouter(app)
|
setupRouter(app)
|
||||||
|
|
||||||
app.mount('#app', true)
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap()
|
setupApp()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createPageLoadingGuard } from './pageLoadingGuard'
|
import { createPageLoadingGuard } from './page-loading-guard'
|
||||||
import { createPermissionGuard } from './permissionGuard'
|
import { createPageTitleGuard } from './page-title-guard'
|
||||||
|
import { createPermissionGuard } from './permission-guard'
|
||||||
|
|
||||||
export function setupRouterGuard(router) {
|
export function setupRouterGuard(router) {
|
||||||
createPageLoadingGuard(router)
|
createPageLoadingGuard(router)
|
||||||
createPermissionGuard(router)
|
createPermissionGuard(router)
|
||||||
|
createPageTitleGuard(router)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export function createPageLoadingGuard(router) {
|
export function createPageLoadingGuard(router) {
|
||||||
router.beforeEach(() => {
|
router.beforeEach(() => {
|
||||||
$loadingBar.start()
|
window.$loadingBar?.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$loadingBar.finish()
|
window.$loadingBar?.finish()
|
||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.onError(() => {
|
router.onError(() => {
|
||||||
$loadingBar.error()
|
window.$loadingBar?.error()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
12
src/router/guard/page-title-guard.js
Normal file
12
src/router/guard/page-title-guard.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const baseTitle = import.meta.env.VITE_TITLE
|
||||||
|
|
||||||
|
export function createPageTitleGuard(router) {
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const pageTitle = to.meta?.title
|
||||||
|
if (pageTitle) {
|
||||||
|
document.title = `${pageTitle} | ${baseTitle}`
|
||||||
|
} else {
|
||||||
|
document.title = baseTitle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
42
src/router/guard/permission-guard.js
Normal file
42
src/router/guard/permission-guard.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
|
import { NOT_FOUND_ROUTE } from '@/router/routes'
|
||||||
|
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
|
||||||
|
import { toLogin } from '@/utils/auth'
|
||||||
|
import { isNullOrWhitespace } from '@/utils/is'
|
||||||
|
|
||||||
|
const WHITE_LIST = ['/login']
|
||||||
|
export function createPermissionGuard(router) {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const permissionStore = usePermissionStore()
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
const token = getToken()
|
||||||
|
|
||||||
|
/** 没有token的情况 */
|
||||||
|
if (isNullOrWhitespace(token)) {
|
||||||
|
if (WHITE_LIST.includes(to.path)) return true
|
||||||
|
return { path: 'login', query: { ...to.query, redirect: to.path } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 有token的情况 */
|
||||||
|
|
||||||
|
if (to.path === '/login') return { path: '/', replace: true }
|
||||||
|
|
||||||
|
// 已经拿到用户信息
|
||||||
|
if (userStore.userId) {
|
||||||
|
refreshAccessToken()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
await userStore.getUserInfo().catch((error) => {
|
||||||
|
removeToken()
|
||||||
|
toLogin()
|
||||||
|
$message.error(error.message || '获取用户信息失败!')
|
||||||
|
})
|
||||||
|
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||||
|
accessRoutes.forEach((route) => {
|
||||||
|
!router.hasRoute(route.name) && router.addRoute(route)
|
||||||
|
})
|
||||||
|
router.addRoute(NOT_FOUND_ROUTE)
|
||||||
|
return { ...to, replace: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
|
||||||
import { NOT_FOUND_ROUTE } from '@/router/routes'
|
|
||||||
import { getToken, refreshAccessToken, removeToken } from '@/utils/token'
|
|
||||||
|
|
||||||
const WHITE_LIST = ['/login', '/redirect']
|
|
||||||
export function createPermissionGuard(router) {
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const permissionStore = usePermissionStore()
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
const token = getToken()
|
|
||||||
if (token) {
|
|
||||||
if (to.path === '/login') {
|
|
||||||
next({ path: '/' })
|
|
||||||
} else {
|
|
||||||
if (userStore.userId) {
|
|
||||||
// 已经拿到用户信息
|
|
||||||
refreshAccessToken()
|
|
||||||
next()
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await userStore.getUserInfo()
|
|
||||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
|
||||||
accessRoutes.forEach((route) => {
|
|
||||||
!router.hasRoute(route.name) && router.addRoute(route)
|
|
||||||
})
|
|
||||||
router.addRoute(NOT_FOUND_ROUTE)
|
|
||||||
next({ ...to, replace: true })
|
|
||||||
} catch (error) {
|
|
||||||
removeToken()
|
|
||||||
$message.error(error)
|
|
||||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (WHITE_LIST.includes(to.path)) {
|
|
||||||
next()
|
|
||||||
} else {
|
|
||||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
import { setupRouterGuard } from './guard'
|
import { setupRouterGuard } from './guard'
|
||||||
import { basicRoutes } from './routes'
|
import { basicRoutes } from './routes'
|
||||||
|
|
||||||
|
const isHash = import.meta.env.VITE_USE_HASH === 'true'
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory('/'),
|
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||||
routes: basicRoutes,
|
routes: [],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
export function resetRouter() {
|
export function resetRouter() {
|
||||||
router.getRoutes().forEach((route) => {
|
router.getRoutes().forEach((route) => {
|
||||||
const { name } = route
|
const { name } = route
|
||||||
if (name && !WHITE_NAME_LIST.includes(name)) {
|
router.hasRoute(name) && router.removeRoute(name)
|
||||||
router.hasRoute(name) && router.removeRoute(name)
|
})
|
||||||
}
|
basicRoutes.forEach((route) => {
|
||||||
|
!router.hasRoute(route.name) && router.addRoute(route)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRouter(app) {
|
export function setupRouter(app) {
|
||||||
|
basicRoutes.forEach((route) => {
|
||||||
|
!router.hasRoute(route.name) && router.addRoute(route)
|
||||||
|
})
|
||||||
app.use(router)
|
app.use(router)
|
||||||
setupRouterGuard(router)
|
setupRouterGuard(router)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Layout from '@/layout/index.vue'
|
const Layout = () => import('@/layout/index.vue')
|
||||||
import Dashboard from '@/views/dashboard/index.vue'
|
|
||||||
|
|
||||||
export const basicRoutes = [
|
export const basicRoutes = [
|
||||||
{
|
{
|
||||||
@@ -8,28 +7,9 @@ export const basicRoutes = [
|
|||||||
component: () => import('@/views/error-page/404.vue'),
|
component: () => import('@/views/error-page/404.vue'),
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: '401',
|
|
||||||
path: '/401',
|
|
||||||
component: () => import('@/views/error-page/401.vue'),
|
|
||||||
isHidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'REDIRECT',
|
|
||||||
path: '/redirect',
|
|
||||||
component: Layout,
|
|
||||||
isHidden: true,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'REDIRECT_NAME',
|
|
||||||
path: '',
|
|
||||||
component: () => import('@/views/redirect/index.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'LOGIN',
|
name: 'Login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
component: () => import('@/views/login/index.vue'),
|
component: () => import('@/views/login/index.vue'),
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
@@ -39,56 +19,37 @@ export const basicRoutes = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'HOME',
|
name: 'ExternalLink',
|
||||||
path: '/',
|
path: '/external-link',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/dashboard',
|
|
||||||
meta: {
|
meta: {
|
||||||
title: '首页',
|
title: '外部链接',
|
||||||
|
icon: 'mdi:link-variant',
|
||||||
|
order: 2,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'DASHBOARD',
|
name: 'LinkGithubSrc',
|
||||||
path: 'dashboard',
|
path: 'https://github.com/zclzone/vue-naive-admin',
|
||||||
component: Dashboard,
|
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Dashboard',
|
title: '源码 - github',
|
||||||
},
|
icon: 'mdi:github',
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'TEST',
|
|
||||||
path: '/test',
|
|
||||||
component: Layout,
|
|
||||||
redirect: '/test/unocss',
|
|
||||||
meta: {
|
|
||||||
title: '测试',
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'UNOCSS',
|
|
||||||
path: 'unocss',
|
|
||||||
component: () => import('@/views/test-page/TestUnocss.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '测试unocss',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MESSAGE',
|
name: 'LinkGiteeSrc',
|
||||||
path: 'message',
|
path: 'https://gitee.com/zclzone/vue-naive-admin',
|
||||||
component: () => import('@/views/test-page/TestMessage.vue'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: '测试Message',
|
title: '源码 - gitee',
|
||||||
|
icon: 'simple-icons:gitee',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'DIALOG',
|
name: 'LinkDocs',
|
||||||
path: 'dialog',
|
path: 'https://zclzone.github.io/vue-naive-admin-docs',
|
||||||
component: () => import('@/views/test-page/TestDialog.vue'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: '测试Dialog',
|
title: '文档 - vuepress',
|
||||||
|
icon: 'mdi:vuejs',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -96,16 +57,16 @@ export const basicRoutes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const NOT_FOUND_ROUTE = {
|
export const NOT_FOUND_ROUTE = {
|
||||||
name: 'NOT_FOUND',
|
name: 'NotFound',
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
redirect: '/404',
|
redirect: '/404',
|
||||||
isHidden: true,
|
isHidden: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const modules = import.meta.globEager('./modules/*.js')
|
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
|
||||||
const asyncRoutes = []
|
const asyncRoutes = []
|
||||||
Object.keys(modules).forEach((key) => {
|
Object.keys(modules).forEach((key) => {
|
||||||
asyncRoutes.push(...modules[key].default)
|
asyncRoutes.push(modules[key].default)
|
||||||
})
|
})
|
||||||
|
|
||||||
export { asyncRoutes }
|
export { asyncRoutes }
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import Layout from '@/layout/index.vue'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
name: 'USER_MANAGER',
|
|
||||||
path: '/user',
|
|
||||||
component: Layout,
|
|
||||||
redirect: '/user/management',
|
|
||||||
meta: {
|
|
||||||
title: '用户中心',
|
|
||||||
role: ['admin'],
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'USER',
|
|
||||||
path: 'management',
|
|
||||||
component: () => import('@/views/user/index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '用户管理',
|
|
||||||
role: ['admin'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PERMISSION',
|
|
||||||
path: 'permission',
|
|
||||||
component: () => import('@/views/user/UserPermission.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '权限管理',
|
|
||||||
role: ['admin'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -3,15 +3,27 @@ import { defineStore } from 'pinia'
|
|||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
themeOverrides: {
|
reloadFlag: true,
|
||||||
common: {
|
collapsed: false,
|
||||||
primaryColor: '#316c72',
|
|
||||||
primaryColorSuppl: '#316c72',
|
|
||||||
primaryColorHover: '#316c72',
|
|
||||||
successColorHover: '#316c72',
|
|
||||||
successColorSuppl: '#316c72',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
async reloadPage() {
|
||||||
|
$loadingBar.start()
|
||||||
|
this.reloadFlag = false
|
||||||
|
await nextTick()
|
||||||
|
this.reloadFlag = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.documentElement.scrollTo({ left: 0, top: 0 })
|
||||||
|
$loadingBar.finish()
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
switchCollapsed() {
|
||||||
|
this.collapsed = !this.collapsed
|
||||||
|
},
|
||||||
|
setCollapsed(collapsed) {
|
||||||
|
this.collapsed = collapsed
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { defineStore } from 'pinia'
|
|||||||
import { asyncRoutes, basicRoutes } from '@/router/routes'
|
import { asyncRoutes, basicRoutes } from '@/router/routes'
|
||||||
|
|
||||||
function hasPermission(route, role) {
|
function hasPermission(route, role) {
|
||||||
|
// * 不需要权限直接返回true
|
||||||
|
if (!route.meta?.requireAuth) return true
|
||||||
|
|
||||||
const routeRole = route.meta?.role ? route.meta.role : []
|
const routeRole = route.meta?.role ? route.meta.role : []
|
||||||
if (!role.length || !routeRole.length) {
|
|
||||||
return false
|
// * 登录用户没有角色或者路由没有设置角色判定为没有权限
|
||||||
}
|
if (!role.length || !routeRole.length) return false
|
||||||
|
|
||||||
|
// * 路由指定的角色包含任一登录用户角色则判定有权限
|
||||||
return role.some((item) => routeRole.includes(item))
|
return role.some((item) => routeRole.includes(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +43,9 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
routes() {
|
routes() {
|
||||||
return basicRoutes.concat(this.accessRoutes)
|
return basicRoutes.concat(this.accessRoutes)
|
||||||
},
|
},
|
||||||
|
menus() {
|
||||||
|
return this.routes.filter((route) => route.name && !route.isHidden)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
generateRoutes(role = []) {
|
generateRoutes(role = []) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user