Compare commits
265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f3d51c1d | ||
|
|
c9425d8803 | ||
|
|
204a7b3a9b | ||
|
|
417f85b8ed | ||
|
|
b47f3d35ca | ||
|
|
91dd2059f8 | ||
|
|
c71a717b8d | ||
|
|
89c3923874 | ||
|
|
f4be4bfb36 | ||
|
|
33da6652a6 | ||
|
|
4be6e4a3ca | ||
|
|
08b30bc864 | ||
|
|
84d8377816 | ||
|
|
d25941befe | ||
|
|
0490e4ded6 | ||
|
|
3ca70e11e1 | ||
|
|
e149dc90b4 | ||
|
|
2a61038fe3 | ||
|
|
7a9b1b2d34 | ||
|
|
9e3c434630 | ||
|
|
63cc4a8b7b | ||
|
|
d1ed8bb933 | ||
|
|
be1884bd05 | ||
|
|
60942e82f3 | ||
|
|
db56ee536b | ||
|
|
53de707509 | ||
|
|
60d0162ced | ||
|
|
4fff446dca | ||
|
|
a4dd39c5db | ||
|
|
f76a3c02c2 | ||
|
|
07d5ab4837 | ||
|
|
08a11b3058 | ||
|
|
921e0d18e9 | ||
|
|
3e259877c6 | ||
|
|
a431a2cf85 | ||
|
|
c7c8691164 | ||
|
|
1d246d1cd6 | ||
|
|
1854d2cec4 | ||
|
|
478d8fccd1 | ||
|
|
0accfe3a4a | ||
|
|
4bd50e2f9e | ||
|
|
72857b3862 | ||
|
|
a18ffce56c | ||
|
|
a2222c4dc0 | ||
|
|
a357ac36ee | ||
|
|
5e1acd7b8e | ||
|
|
8bbd04e668 | ||
|
|
0c5c3df645 | ||
|
|
ed577cd41a | ||
|
|
456ad78dd2 | ||
|
|
ba20e7a96b | ||
|
|
3ed1aafa80 | ||
|
|
5cf1212847 | ||
|
|
81e0bb7b78 | ||
|
|
f3125ddec8 | ||
|
|
3b3cb7ba34 | ||
|
|
d7c1063102 | ||
|
|
a5aa8a353f | ||
|
|
cbb6ca4f6b | ||
|
|
2ece015dae | ||
|
|
a80f83a011 | ||
|
|
65d4d3848d | ||
|
|
53830256f4 | ||
|
|
22c59c208f | ||
|
|
b43f87035b | ||
|
|
20eee94630 | ||
|
|
c504ad065c | ||
|
|
0e764ac748 | ||
|
|
8f15e1c655 | ||
|
|
d702a6703b | ||
|
|
d49af8b574 | ||
|
|
a036602d3e | ||
|
|
eb173dc38e | ||
|
|
e5f1ee25c3 | ||
|
|
fe11e18197 | ||
|
|
0f9fb9f1c9 | ||
|
|
50f96b99c7 | ||
|
|
2eb936bcac | ||
|
|
b50731881c | ||
|
|
d4a5cffd81 | ||
|
|
70eab22f65 | ||
|
|
0247f3ebfa | ||
|
|
a610c1c6d0 | ||
|
|
5a9c0fb584 | ||
|
|
cad72b3b73 | ||
|
|
9edd0e5ad6 | ||
|
|
ae6db3ed3c | ||
|
|
dcab55055c | ||
|
|
5aa4d3d5ae | ||
|
|
eea9fc79f7 | ||
|
|
526792e22f | ||
|
|
855202962c | ||
|
|
74a58fafb9 | ||
|
|
ad451dae1e | ||
|
|
35ed004b2e | ||
|
|
fcdd31c935 | ||
|
|
386d9ec27a | ||
|
|
e84dd01365 | ||
|
|
6da56ec881 | ||
|
|
4e9e3469b0 | ||
|
|
3b86597eff | ||
|
|
ebffe52c7c | ||
|
|
6d863e1a63 | ||
|
|
b2e1c2d22c | ||
|
|
2e283c3b19 | ||
|
|
6300758fd0 | ||
|
|
b5717f6b8d | ||
|
|
ff11cdf73f | ||
|
|
9a01e42915 | ||
|
|
7c74578afc | ||
|
|
a6a8002c59 | ||
|
|
daf747348e | ||
|
|
60b5ce1817 | ||
|
|
4c75be67f2 | ||
|
|
be1c875a72 | ||
|
|
329a6e29cb | ||
|
|
af907932fb | ||
|
|
681b3144d1 | ||
|
|
8304970a59 | ||
|
|
5cef8e4a01 | ||
|
|
0b50e1dbee | ||
|
|
88a93c4e57 | ||
|
|
38ae35ee95 | ||
|
|
c7471a66db | ||
|
|
a4531be904 | ||
|
|
c58605de54 | ||
|
|
c3dc0b4b2c | ||
|
|
2d3e9988ec | ||
|
|
9548a0bfc8 | ||
|
|
dda778fdde | ||
|
|
98e3f13185 | ||
|
|
7e79c51630 | ||
|
|
181aed4897 | ||
|
|
c626d2b785 | ||
|
|
ed79e81b13 | ||
|
|
264119a142 | ||
|
|
4c1c77821f | ||
|
|
67b11f04fc | ||
|
|
649fe1d4e8 | ||
|
|
c3192423c6 | ||
|
|
911fc74305 | ||
|
|
2fcfd6b4d1 | ||
|
|
bac868d071 | ||
|
|
ea5460488a | ||
|
|
61d42ead21 | ||
|
|
a98555beb1 | ||
|
|
820eb516ce | ||
|
|
6cd0dc1eff | ||
|
|
5dcb2958a1 | ||
|
|
c25476278b | ||
|
|
99ddb4fe70 | ||
|
|
82c47ffc72 | ||
|
|
1f1678800f | ||
|
|
efdd89cd50 | ||
|
|
100b91a118 | ||
|
|
26b71f0ec6 | ||
|
|
92e7ada37b | ||
|
|
2d879d0592 | ||
|
|
8806a6cb43 | ||
|
|
85a04fd06d | ||
|
|
4a5b8dd005 | ||
|
|
2f7da255e5 | ||
|
|
6664ae8f7b | ||
|
|
bdbe9b8483 | ||
|
|
30211e14ea | ||
|
|
e7b1896d9e | ||
|
|
a5c1046e67 | ||
|
|
31670cd671 | ||
|
|
b0e3a94e12 | ||
|
|
2b2a324a62 | ||
|
|
40483e09e6 | ||
|
|
a5a3472486 | ||
|
|
fd1752693a | ||
|
|
2f3a83758a | ||
|
|
738212c84b | ||
|
|
a4f3e16007 | ||
|
|
5b2d1c68dd | ||
|
|
7b8b50322c | ||
|
|
bb171866b6 | ||
|
|
f1bc9edbac | ||
|
|
3a38adc71e | ||
|
|
b760cc34dd | ||
|
|
b59e47b5dd | ||
|
|
d1dd58215d | ||
|
|
661aed1a94 | ||
|
|
f2e2fc6819 | ||
|
|
9ea8ffd7fd | ||
|
|
af983d16b9 | ||
|
|
079761b6fd | ||
|
|
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 |
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: '|'
|
||||
}
|
||||
2
.env
@@ -1,3 +1,3 @@
|
||||
VITE_APP_TITLE = 'Vue Naive Admin'
|
||||
VITE_TITLE = 'Vue Naive Admin'
|
||||
|
||||
VITE_PORT = 3100
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# proxy
|
||||
VITE_PROXY = [["/api","http://localhost:8080"],["/api-test","localhost:8080"]]
|
||||
# 是否启用MOCK
|
||||
VITE_USE_PROXY = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
|
||||
11
.env.github
@@ -1,16 +1,13 @@
|
||||
# 自定义域名CNAME
|
||||
# VITE_APP_GLOB_CNAME = 'template.qszone.com'
|
||||
# VITE_CNAME = 'template.isme.top'
|
||||
|
||||
# 资源公共路径,需要以 /开头和结尾
|
||||
VITE_PUBLIC_PATH = '/vue-naive-admin/'
|
||||
|
||||
VITE_APP_USE_HASH = true
|
||||
VITE_USE_HASH = true
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
VITE_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
# 是否启用压缩
|
||||
VITE_USE_COMPRESS = false
|
||||
|
||||
# 压缩类型
|
||||
VITE_COMPRESS_TYPE = gzip
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
VITE_PUBLIC_PATH = '/'
|
||||
|
||||
# 是否启用MOCK
|
||||
VITE_APP_USE_MOCK = true
|
||||
VITE_USE_MOCK = true
|
||||
|
||||
# base api
|
||||
VITE_APP_BASE_API = '/api'
|
||||
|
||||
# test base api
|
||||
VITE_APP_BASE_API_TEST = '/api-test'
|
||||
VITE_BASE_API = '/api'
|
||||
62
.eslint-global-variables.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"globals": {
|
||||
"$loadingBar": true,
|
||||
"$message": true,
|
||||
"defineOptions": true,
|
||||
"$dialog": true,
|
||||
"$notification": true,
|
||||
"EffectScope": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
15
.eslintrc.js
@@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
'vue/valid-template-root': 'off',
|
||||
'vue/no-multiple-template-root': 'off',
|
||||
'vue/multi-word-component-names': [
|
||||
'error',
|
||||
{
|
||||
ignores: ['index', '401', '404'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ['https://afdian.net/a/isme-admin']
|
||||
38
.github/workflows/deploy.yml
vendored
@@ -1,38 +0,0 @@
|
||||
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
|
||||
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
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit "$1"
|
||||
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
14
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "一键启动",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run-script", "dev"],
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
28
.vscode/settings.json
vendored
@@ -1,12 +1,30 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.printWidth": 120,
|
||||
"prettier.singleQuote": true,
|
||||
"prettier.semi": false,
|
||||
"prettier.endOfLine": "lf",
|
||||
"files.eol": "\n",
|
||||
"path-intellisense.mappings": {
|
||||
"@/": "${workspaceRoot}/src"
|
||||
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"cssrem.rootFontSize": 4, // 适配unocss,1rem = 4px ==> 0.25rem = 1px
|
||||
"files.associations": {
|
||||
"*.env.*": "dotenv",
|
||||
"*.css": "postcss"
|
||||
}
|
||||
}
|
||||
|
||||
208
README.EN.md
Normal file
@@ -0,0 +1,208 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/zclzone/vue-naive-admin">
|
||||
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<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">中文</a>
|
||||
</p>
|
||||
|
||||
### 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.
|
||||
|
||||
### Features
|
||||
|
||||
- 🍒 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`.
|
||||
|
||||
### Preview
|
||||
|
||||
[https://template.isme.top](https://template.isme.top)
|
||||
|
||||
[https://base.isme.top](https://base.isme.top)
|
||||
|
||||
### Docs
|
||||
|
||||
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||
|
||||
|
||||
### Getting Started
|
||||
|
||||
```shell
|
||||
# Recommended setup git autocrlf 为 false
|
||||
git config --global core.autocrlf false
|
||||
|
||||
# Clone Project
|
||||
git clone https://github.com/zclzone/vue-naive-admin.git
|
||||
|
||||
cd vue-naive-admin
|
||||
|
||||
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
|
||||
npm i -g pnpm # Installed and can be ignored
|
||||
pnpm i # or npm i
|
||||
|
||||
# Start
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Build and Release
|
||||
|
||||
```shell
|
||||
# Test Environment
|
||||
pnpm build:test
|
||||
|
||||
# Github Environment
|
||||
pnpm build:github
|
||||
|
||||
# Prod Environment
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
```shell
|
||||
# eslint check
|
||||
pnpm lint
|
||||
|
||||
# eslint check and fix
|
||||
pnpm lint:fix
|
||||
|
||||
# Preview(Need to build first)
|
||||
pnpm preview
|
||||
|
||||
# Commit(husky+commitlint)
|
||||
pnpm cz
|
||||
```
|
||||
|
||||
### Directory description
|
||||
|
||||
```
|
||||
Vue Naive Admin
|
||||
|-- .github // github相关,如推送github仓库后自动部署gh pages
|
||||
|-- .husky // git commit钩子
|
||||
|-- .vscode // vscode编辑器相关
|
||||
| |-- extensions.json // 插件推荐
|
||||
| |-- settings.json // 项目级别的vscode配置,优先级大于全局vscode配置
|
||||
|-- build // 构建相关配置
|
||||
| |-- constant.js // 构建相关的常量
|
||||
| |-- utils.js // 构建相关的工具方法
|
||||
| |-- config
|
||||
| | |-- define.js // 注入全局常量,启动或打包后将添加到window中
|
||||
| | |-- proxy.js // 代理配置
|
||||
| |-- plugin
|
||||
| | |-- html.js // vite-plugin-html插件,用于注入变量或者html标签
|
||||
| | |-- mock.js // vite-plugin-mock插件,处理mock
|
||||
| | |-- unplugin.js // unplugin相关插件,包含DefineOptions和自动导入
|
||||
| |-- script // 打包完成后执行的一些node脚本(不重要)
|
||||
| |-- build-cname.js // 自动生成cname
|
||||
|-- mock // mock
|
||||
| |-- utils.js // mock请求需要用到的工具方法
|
||||
| |-- api // mock接口
|
||||
|-- public // 公共资源,文件夹下的文件会在打包后会直接加到dist根目录下
|
||||
|-- settings // 项目配置
|
||||
| |-- proxy-config.js // 代理配置文件
|
||||
| |-- theme.json // 主题配置项,主题色等
|
||||
|-- src
|
||||
| |-- api // 公共api
|
||||
| |-- assets // 静态资源
|
||||
| | |-- images // 图片
|
||||
| | |-- svg // svg图标
|
||||
| |-- components // 全局组件
|
||||
| | |-- common // 公共组件
|
||||
| | |-- icon // icon相关组件
|
||||
| | |-- page // 页面组件
|
||||
| | |-- query-bar // 查询选项
|
||||
| | |-- table // 封装的表格组件
|
||||
| |-- composables // 封装的组合式函数
|
||||
| |-- layout // 布局相关组件
|
||||
| | |-- components
|
||||
| | |-- AppMain.vue // 主体内容
|
||||
| | |-- header // 头部
|
||||
| | |-- sidebar // 侧边菜单栏
|
||||
| | |-- tags // 多页签栏
|
||||
| |-- router // 路由
|
||||
| | |-- guard // 路由守卫
|
||||
| | |-- routes // 路由列表
|
||||
| |-- store // 状态管理(pinia)
|
||||
| | |-- modules // 模块
|
||||
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
|
||||
| | |-- permission // 权限相关,管理权限菜单
|
||||
| | |-- tags // 管理多页签
|
||||
| | |-- user // 用户模块,管理用户信息、登录登出
|
||||
| |-- styles // 样式
|
||||
| |-- utils // 封装的工具方法
|
||||
| | |-- auth // 权限相关,如token、跳转登录页等
|
||||
| | |-- common // 通用
|
||||
| | |-- http // 封装axios
|
||||
| | |-- storage // 封装localStorage和sessionStorage
|
||||
| |-- views // 页面
|
||||
| | |-- demo // 示例
|
||||
| | |-- error-page // 错误页
|
||||
| | |-- login // 登录页
|
||||
| | |-- workbench // 首页
|
||||
| |-- App.vue
|
||||
| |-- main.js
|
||||
|-- .cz-config.js // git提交配置
|
||||
|-- .editorconfig // 编辑器配置
|
||||
|-- .env // 环境文件,所有环境都会载入
|
||||
|-- .env.development // 开发环境文件
|
||||
|-- .env.production // 生产环境文件
|
||||
|-- .env.test // 测试环境文件
|
||||
|-- .eslintignore // eslint忽略
|
||||
|-- .eslintrc.js // eslint配置
|
||||
|-- .gitignore // git忽略
|
||||
|-- .prettierignore // prettier格式化忽略
|
||||
|-- commitlint.config.js // commitlint规范配置
|
||||
|-- index.html
|
||||
|-- jsconfig.json // js配置
|
||||
|-- LICENSE // 协议
|
||||
|-- package.json // 依赖描述文件
|
||||
|-- pnpm-lock.yaml // 依赖锁定文件
|
||||
|-- prettier.config.js // prettier格式化配置
|
||||
|-- README.md // 项目描述文档(英文)
|
||||
|-- README.zh-CN.md // 项目描述文档(中文)
|
||||
|-- unocss.config.js // unocss配置
|
||||
|-- vite.config.js // vite配置
|
||||
```
|
||||
|
||||
### TS version: Qs Admin
|
||||
|
||||
#### source code
|
||||
|
||||
- github: [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.isme.top](https://admin.isme.top)
|
||||
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||
|
||||
### Open source projects that use this project:
|
||||
|
||||
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): A full-stack blog project in Golang, the frontend of the blog backend is based on vue-naive-admin and integrates with a real backend service, implementing features such as backend-controlled routing.
|
||||
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): A Python backend management project that integrates RBAC permission management, dynamic routing, and JWT authentication, helping small and medium-sized applications to quickly establish a foundation.
|
||||
|
||||
### Communication group & About the author
|
||||
|
||||
<a href="https://blog.isme.top/about/">
|
||||
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
219
README.md
@@ -1,53 +1,61 @@
|
||||
<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">
|
||||
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
|
||||
</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>
|
||||
<a href="./LICENSE"><img alt="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.EN.md">English</a>
|
||||
</p>
|
||||
|
||||
> 🎉🎉🎉 2.0 已开源,全新重构,全面简化,后端使用 nestjs + mysql + typeOrm,[👉点击前往2.0版本 | 分支 2.x](https://github.com/zclzone/vue-naive-admin/tree/2.x),
|
||||
|
||||
- 体验地址: [admin.isme.top](https://admin.isme.top)
|
||||
- 后端服务: [isme-nest-serve](https://github.com/zclzone/isme-nest-serve)
|
||||
- 文档: [vue-naive-admin-docs](https://docs.isme.top/web/#/624306705)
|
||||
|
||||
### 简介
|
||||
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin),一个基于 Vue3.0、Vite、Naive UI 的后台管理模板,相较于其他比较流行的后台管理模板,此项目相对简洁、轻量,学习成本非常低,对新手极其友好。不过麻雀虽小五脏俱全,权限、Mock、菜单、axios 封装、pinia、项目配置、样式配置、环境配置,以及一些经常用的基础组件封装等等这些该有的都有,非常适用于中小型项目或者个人项目,也可此模板进行二次封装改造用于大型项目。
|
||||
|
||||
### 为什么要开发这个模板
|
||||
|
||||
- Vue3 和 Vite 已经趋于成熟,学习 vite 和 vue3 非常有必要,通过开发模板进行学习是一个很好的方式,事实也证明我确实从中获益良多
|
||||
- 目前主流的 Vue3+Vite 后台管理模板都相对复杂,甚至感觉有点花里胡哨(没有贬低的意思,大部分的架构设计都很优秀,只是觉得集成了太多不实用的东西)
|
||||
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite4、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,上手成本非常低,非常适合中小型项目或者个人项目。
|
||||
|
||||
### 功能
|
||||
|
||||
- 🍒 集成 Naive UI,尤大推荐的 UI 组件库,[https://www.naiveui.com](https://www.naiveui.com)
|
||||
- 🍒 集成 [Naive UI](https://www.naiveui.com)
|
||||
- 🍑 集成登陆、注销及权限验证
|
||||
- 🍐 集成多环境配置,dev、测试、生产和github pages环境
|
||||
- 🍎 集成 Eslint + Prettier,代码约束和格式化统一
|
||||
- 🍉 集成 Mock 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||
- 🍇 集成 unocss,antfu 大神开源的原子化 css 解决方案,非常轻量,目前我是自己写 scss 样式搭配着 unocss 使用的
|
||||
- 🍍 集成 Pinia,Vuex 的替代方案,轻量、简单、易用(尤大已表示不会有Vuex5,或者说pinia就是Vuex5)
|
||||
- 📦 集成 Vite 自动导入插件unplugin-vue-components,解放双手,开发效率直接起飞
|
||||
- 🤹 集成 unplugin-icons插件,优雅使用iconify图标
|
||||
- 🍏 二次封装 Axios,支持多 axios 实例
|
||||
- 🍌 二次封装全局 Dialog、Message、LoadingBar 组件
|
||||
- 🍋 二次封装 localStorage 和 sessionStorage,支持设置过期时间
|
||||
- 🍐 集成多环境配置,dev、测试、生产环境
|
||||
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
|
||||
- 🍌 集成 `husky + commitlint`,代码提交规范化
|
||||
- 🍉 集成 `mock` 接口服务,dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
|
||||
- 🍍 集成 `pinia`,vuex 的替代方案,轻量、简单、易用
|
||||
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
|
||||
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
|
||||
- 🍇 集成 `unocss`,antfu 开源的原子 css 解决方案,非常轻量
|
||||
|
||||
> ✨✨ 双十一香港特惠服务器推荐,~~**2C4G 100M** `71/年` `142/两年`~~,[👉点击前往](https://blog.isme.top/vps-recommend/)
|
||||
|
||||
### 预览
|
||||
|
||||
[template.qszone.com](https://template.qszone.com)
|
||||
[https://template.isme.top](https://template.isme.top)
|
||||
|
||||
[github pages](https://zclzone.github.io/vue-naive-admin)
|
||||
[https://base.isme.top](https://base.isme.top)
|
||||
|
||||
### 文档
|
||||
|
||||
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||
项目文档: [Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
|
||||
|
||||
[羽雀文档:Vue Naive Admin](https://www.yuque.com/qszone/vue-naive-admin)
|
||||
从0到1搭建后台: [从0到1,带你搭建Vite+Vue3+Pinia+Naive UI后台](https://juejin.cn/column/7093180796424421384)
|
||||
|
||||
### 构建
|
||||
如何安装pnpm: [安装pnpm](docs/安装pnpm.md)
|
||||
|
||||
如何使用图标: [使用图标](docs/使用图标.md)
|
||||
|
||||
如何使用unocss: [保熟的UnoCSS使用指北,优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
|
||||
|
||||
### 快速开始
|
||||
|
||||
```shell
|
||||
# 推荐配置git autocrlf 为 false(本项目规范使用lf换行符,此配置是为防止git自动将源文件转换为crlf)
|
||||
@@ -61,62 +69,161 @@ 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
|
||||
|
||||
# 启动
|
||||
npm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 发布
|
||||
### 构建发布
|
||||
|
||||
```shell
|
||||
# 构建测试环境
|
||||
npm run build:test
|
||||
pnpm build:test
|
||||
|
||||
# 构建github pages环境
|
||||
npm run build:github
|
||||
pnpm build:github
|
||||
|
||||
# 构建生产环境
|
||||
npm run build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 其他指令
|
||||
|
||||
```shell
|
||||
# eslint代码格式检查
|
||||
npm run lint
|
||||
pnpm lint
|
||||
|
||||
# 代码检查并修复
|
||||
npm run lint:fix
|
||||
pnpm lint:fix
|
||||
|
||||
# 预览发布包效果(需先执行构建指令)
|
||||
npm run preview
|
||||
pnpm preview
|
||||
|
||||
# 提交代码(husky+commitlint)
|
||||
pnpm cz
|
||||
```
|
||||
|
||||
### 规范
|
||||
|
||||
#### git commit 规范
|
||||
### 目录说明
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
- `mod` 不确定分类的修改
|
||||
- `release` 发布
|
||||
```
|
||||
Vue Naive Admin
|
||||
|-- .github // github相关,如推送github仓库后自动部署gh pages
|
||||
|-- .husky // git commit钩子
|
||||
|-- .vscode // vscode编辑器相关
|
||||
| |-- extensions.json // 插件推荐
|
||||
| |-- settings.json // 项目级别的vscode配置,优先级大于全局vscode配置
|
||||
|-- build // 构建相关配置
|
||||
| |-- constant.js // 构建相关的常量
|
||||
| |-- utils.js // 构建相关的工具方法
|
||||
| |-- config
|
||||
| | |-- define.js // 注入全局常量,启动或打包后将添加到window中
|
||||
| | |-- proxy.js // 代理配置
|
||||
| |-- plugin
|
||||
| | |-- html.js // vite-plugin-html插件,用于注入变量或者html标签
|
||||
| | |-- mock.js // vite-plugin-mock插件,处理mock
|
||||
| | |-- unplugin.js // unplugin相关插件,包含DefineOptions和自动导入
|
||||
| |-- script // 打包完成后执行的一些node脚本(不重要)
|
||||
| |-- build-cname.js // 自动生成cname
|
||||
|-- mock // mock
|
||||
| |-- utils.js // mock请求需要用到的工具方法
|
||||
| |-- api // mock接口
|
||||
|-- public // 公共资源,文件夹下的文件会在打包后会直接加到dist根目录下
|
||||
|-- settings // 项目配置
|
||||
| |-- proxy-config.js // 代理配置文件
|
||||
| |-- theme.json // 主题配置项,主题色等
|
||||
|-- src
|
||||
| |-- api // 公共api
|
||||
| |-- assets // 静态资源
|
||||
| | |-- images // 图片
|
||||
| | |-- svg // svg图标
|
||||
| |-- components // 全局组件
|
||||
| | |-- common // 公共组件
|
||||
| | |-- icon // icon相关组件
|
||||
| | |-- page // 页面组件
|
||||
| | |-- query-bar // 查询选项
|
||||
| | |-- table // 封装的表格组件
|
||||
| |-- composables // 封装的组合式函数
|
||||
| |-- layout // 布局相关组件
|
||||
| | |-- components
|
||||
| | |-- AppMain.vue // 主体内容
|
||||
| | |-- header // 头部
|
||||
| | |-- sidebar // 侧边菜单栏
|
||||
| | |-- tags // 多页签栏
|
||||
| |-- router // 路由
|
||||
| | |-- guard // 路由守卫
|
||||
| | |-- routes // 路由列表
|
||||
| |-- store // 状态管理(pinia)
|
||||
| | |-- modules // 模块
|
||||
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
|
||||
| | |-- permission // 权限相关,管理权限菜单
|
||||
| | |-- tags // 管理多页签
|
||||
| | |-- user // 用户模块,管理用户信息、登录登出
|
||||
| |-- styles // 样式
|
||||
| |-- utils // 封装的工具方法
|
||||
| | |-- auth // 权限相关,如token、跳转登录页等
|
||||
| | |-- common // 通用
|
||||
| | |-- http // 封装axios
|
||||
| | |-- storage // 封装localStorage和sessionStorage
|
||||
| |-- views // 页面
|
||||
| | |-- demo // 示例
|
||||
| | |-- error-page // 错误页
|
||||
| | |-- login // 登录页
|
||||
| | |-- workbench // 首页
|
||||
| |-- App.vue
|
||||
| |-- main.js
|
||||
|-- .cz-config.js // git提交配置
|
||||
|-- .editorconfig // 编辑器配置
|
||||
|-- .env // 环境文件,所有环境都会载入
|
||||
|-- .env.development // 开发环境文件
|
||||
|-- .env.production // 生产环境文件
|
||||
|-- .env.test // 测试环境文件
|
||||
|-- .eslintignore // eslint忽略
|
||||
|-- .eslintrc.js // eslint配置
|
||||
|-- .gitignore // git忽略
|
||||
|-- .prettierignore // prettier格式化忽略
|
||||
|-- commitlint.config.js // commitlint规范配置
|
||||
|-- index.html
|
||||
|-- jsconfig.json // js配置
|
||||
|-- LICENSE // 协议
|
||||
|-- package.json // 依赖描述文件
|
||||
|-- pnpm-lock.yaml // 依赖锁定文件
|
||||
|-- prettier.config.js // prettier格式化配置
|
||||
|-- README.md // 项目描述文档(英文)
|
||||
|-- README.zh-CN.md // 项目描述文档(中文)
|
||||
|-- unocss.config.js // unocss配置
|
||||
|-- vite.config.js // vite配置
|
||||
```
|
||||
|
||||
### TS 版本: Qs Admin
|
||||
|
||||
#### 源码
|
||||
|
||||
<p align="center">
|
||||
<img src="https://assets.qszone.com/image/Snipaste_2022-06-23_19-26-26.png" />
|
||||
- github: [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.isme.top](https://admin.isme.top)
|
||||
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
|
||||
|
||||
### 使用该项目的开源项目
|
||||
|
||||
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin,对接真实后端服务,实现了后端控制路由等特性。
|
||||
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): Python 后台管理项目, 融合了 RBAC 权限管理、动态路由,JWT 鉴权,助力中小型应用快速搭建。
|
||||
|
||||
### 入群交流 & 关于作者
|
||||
|
||||
<a href="https://blog.isme.top/about/">
|
||||
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
|
||||
</a>
|
||||
|
||||
### ☕ 赞助我
|
||||
|
||||
> 开源不易,请作者喝杯咖啡吧
|
||||
<p>
|
||||
<img src="https://static.isme.top/images/zhifu_weixin.jpg" style="width: 220px" />
|
||||
<img src="https://static.isme.top/images/zhifu_zhifubao.jpg" style="width: 220px" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
export const OUTPUT_DIR = 'dist'
|
||||
|
||||
export const PROXY_CONFIG = {
|
||||
/**
|
||||
* @desc 替换匹配值
|
||||
* @请求路径 http://localhost:3100/api/user
|
||||
* @转发路径 http://localhost:8080/user
|
||||
*/
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
|
||||
},
|
||||
/**
|
||||
* @desc 不替换匹配值
|
||||
* @请求路径 http://localhost:3100/api/v2/user
|
||||
* @转发路径 http://localhost:8080/api/v2/user
|
||||
*/
|
||||
'/api/v2': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
/**
|
||||
* @desc 替换部分匹配值
|
||||
* @请求路径 http://localhost:3100/api/v3/user
|
||||
* @转发路径 http://localhost:8080/user
|
||||
*/
|
||||
'/api/v3': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
|
||||
export function configHtmlPlugin(viteEnv, isBuild) {
|
||||
const { VITE_APP_TITLE, VITE_PUBLIC_PATH } = viteEnv
|
||||
const { VITE_TITLE } = viteEnv
|
||||
|
||||
const htmlPlugin = createHtmlPlugin({
|
||||
minify: isBuild,
|
||||
inject: {
|
||||
data: {
|
||||
title: VITE_APP_TITLE,
|
||||
title: VITE_TITLE,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
/**
|
||||
* * 扩展setup插件,支持在script标签中使用name属性
|
||||
* usage: <script setup name="MyComp"></script>
|
||||
*/
|
||||
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
||||
|
||||
/**
|
||||
* * unocss插件,原子css
|
||||
* https://github.com/antfu/unocss
|
||||
@@ -14,18 +8,26 @@ import Unocss from 'unocss/vite'
|
||||
|
||||
// rollup打包分析插件
|
||||
import visualizer from 'rollup-plugin-visualizer'
|
||||
// 压缩
|
||||
import viteCompression from 'vite-plugin-compression'
|
||||
// vite-vuedevtool
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
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()]
|
||||
const plugins = [VueDevTools(), vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
|
||||
|
||||
if (viteEnv?.VITE_APP_USE_MOCK) {
|
||||
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({
|
||||
|
||||
@@ -2,12 +2,11 @@ import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
export function configMockPlugin(isBuild) {
|
||||
return viteMockServe({
|
||||
ignore: /^\_/,
|
||||
mockPath: 'mock',
|
||||
mockPath: 'mock/api',
|
||||
localEnabled: !isBuild,
|
||||
prodEnabled: isBuild,
|
||||
injectCode: `
|
||||
import { setupProdMockServer } from '../mock/_create-prod-server';
|
||||
import { setupProdMockServer } from '../mock';
|
||||
setupProdMockServer();
|
||||
`,
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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'
|
||||
@@ -10,13 +11,16 @@ import IconsResolver from 'unplugin-icons/resolver'
|
||||
* 图标库: https://icones.js.org/
|
||||
*/
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
|
||||
import { getRootPath } from '../utils'
|
||||
import { getSrcPath } from '../utils'
|
||||
|
||||
const customIconPath = resolve(getSrcPath(), 'assets/svg')
|
||||
|
||||
const customIconPath = getRootPath('src', 'assets/icons')
|
||||
export default [
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router'],
|
||||
dts: false,
|
||||
}),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
@@ -27,6 +31,16 @@ export default [
|
||||
defaultClass: 'inline-block',
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })],
|
||||
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 { writeFileSync } from 'fs-extra'
|
||||
import { OUTPUT_DIR } from '../constant'
|
||||
import { getEnvConfig, getRootPath } from '../utils'
|
||||
|
||||
export function runBuildCNAME() {
|
||||
const { VITE_APP_CNAME } = getEnvConfig()
|
||||
if (!VITE_APP_CNAME) return
|
||||
const { VITE_CNAME } = getEnvConfig()
|
||||
if (!VITE_CNAME) return
|
||||
try {
|
||||
writeFileSync(getRootPath(`${OUTPUT_DIR}/CNAME`), VITE_APP_CNAME)
|
||||
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
|
||||
} catch (error) {
|
||||
console.log(chalk.red('CNAME file failed to package:\n' + error))
|
||||
}
|
||||
|
||||
@@ -2,53 +2,36 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
const httpsReg = /^https:\/\//
|
||||
|
||||
export function wrapperEnv(envOptions) {
|
||||
if (!envOptions) return {}
|
||||
const ret = {}
|
||||
|
||||
for (const key in envOptions) {
|
||||
let val = envOptions[key]
|
||||
if (['true', 'false'].includes(val)) {
|
||||
val = val === 'true'
|
||||
}
|
||||
if (['VITE_PORT'].includes(key)) {
|
||||
val = +val
|
||||
}
|
||||
if (key === 'VITE_PROXY' && val && typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val.replace(/'/g, '"'))
|
||||
} catch (error) {
|
||||
val = ''
|
||||
}
|
||||
}
|
||||
ret[key] = val
|
||||
if (typeof val === 'string') {
|
||||
process.env[key] = val
|
||||
} else if (typeof val === 'object') {
|
||||
process.env[key] = JSON.stringify(val)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
/**
|
||||
* * 项目根路径
|
||||
* @description 结尾不带/
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd())
|
||||
}
|
||||
|
||||
export function createProxy(list = []) {
|
||||
const ret = {}
|
||||
for (const [prefix, target] of list) {
|
||||
const isHttps = httpsReg.test(target)
|
||||
/**
|
||||
* * 项目src路径
|
||||
* @param srcName src目录名称(默认: "src")
|
||||
* @description 结尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
return path.resolve(getRootPath(), srcName)
|
||||
}
|
||||
|
||||
// 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 } : {}),
|
||||
}
|
||||
export function convertEnv(envOptions) {
|
||||
const result = {}
|
||||
if (!envOptions) return result
|
||||
|
||||
for (const envKey in envOptions) {
|
||||
let envVal = envOptions[envKey]
|
||||
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
|
||||
|
||||
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
|
||||
|
||||
result[envKey] = envVal
|
||||
}
|
||||
return ret
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +48,7 @@ function getConfFiles() {
|
||||
return ['.env', '.env.local', '.env.production']
|
||||
}
|
||||
|
||||
export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles()) {
|
||||
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
|
||||
let envConfig = {}
|
||||
confFiles.forEach((item) => {
|
||||
try {
|
||||
@@ -85,7 +68,3 @@ export function getEnvConfig(match = 'VITE_APP_GLOB_', confFiles = getConfFiles(
|
||||
})
|
||||
return envConfig
|
||||
}
|
||||
|
||||
export function getRootPath(...dir) {
|
||||
return path.resolve(process.cwd(), ...dir)
|
||||
}
|
||||
|
||||
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',
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
3
docs/使用unocss.md
Normal file
@@ -0,0 +1,3 @@
|
||||
推荐阅读作者在掘金的文章:
|
||||
|
||||
[保熟的UnoCSS使用指北,优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
|
||||
40
docs/使用图标.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## 使用 iconify 图标
|
||||
|
||||
首先去图标库地址:[icones](https://icones.js.org/) 找合适的图标
|
||||
|
||||
### 1. 结合 unocss 使用
|
||||
|
||||
```html
|
||||
<i i-carbon-sun />
|
||||
<i class="i-carbon-sun" />
|
||||
```
|
||||
|
||||
### 2. 结合插件 unplugin-icons 自定义标签使用
|
||||
|
||||
`<icon-[iconify图标名称]`
|
||||
|
||||
```html
|
||||
<icon-ant-design:fullscreen-exit-outlined />
|
||||
<icon-ant-design:fullscreen-outlined />
|
||||
```
|
||||
|
||||
这种方式还支持自定义 svg 图标,本项目自定义 svg 图标固定放在 src/assets/svg 下
|
||||
|
||||
`<icon-custom-[svg图标文件名]`
|
||||
|
||||
```
|
||||
<icon-custom-logo />
|
||||
```
|
||||
|
||||
具体配置参看 build/plugin/unplugin.js
|
||||
|
||||
### 3. 结合 Naive UI 的 NIcon 组件封装使用
|
||||
|
||||
```html
|
||||
<!-- iconify图标 -->
|
||||
<TheIcon icon="material-symbols:delete-outline" />
|
||||
<!-- 自定义svg图标 -->
|
||||
<TheIcon icon="logo" type="custom" />
|
||||
```
|
||||
|
||||
封装组件参看 src/components/icon
|
||||
32
docs/安装pnpm.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## 安装pnpm
|
||||
|
||||
### 使用Corepack安装(推荐)
|
||||
|
||||
从 v16.13 开始,Node.js 发布了 Corepack 来管理包管理器。 这是一项实验性功能,需要通过运行如下脚本来启用它:
|
||||
|
||||
```
|
||||
npx corepack enable // 可能需要管理员权限
|
||||
```
|
||||
|
||||
这将自动在您的系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。 若要升级,请检查[最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/latest) 并运行,如 7.14.0
|
||||
```
|
||||
corepack prepare pnpm@7.14.0 --activate
|
||||
```
|
||||
|
||||
如果是 Node.js v16.17 或者更新的版本,可以直接安装最新版本的 pnpm
|
||||
```
|
||||
corepack prepare pnpm@latest --activate
|
||||
```
|
||||
|
||||
### 使用npm安装
|
||||
|
||||
```
|
||||
npm i -g pnpm
|
||||
```
|
||||
|
||||
更新,卸了重新装
|
||||
|
||||
```
|
||||
npm uninstall -g pnpm
|
||||
npm i -g pnpm
|
||||
```
|
||||
@@ -7,7 +7,7 @@
|
||||
<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="icon" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/resource/loading.css" />
|
||||
|
||||
<title><%= title %></title>
|
||||
@@ -15,10 +15,9 @@
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<!-- 白屏时的loading效果 -->
|
||||
<div class="loading-container">
|
||||
<div id="loadingLogo" class="loading-svg"></div>
|
||||
<img src="/resource/logo.png" alt="logo" height="128" />
|
||||
<div class="loading-spin__container">
|
||||
<div class="loading-spin">
|
||||
<div class="left-0 top-0 loading-spin-item"></div>
|
||||
@@ -30,7 +29,6 @@
|
||||
<div class="loading-title"><%= title %></div>
|
||||
</div>
|
||||
<script src="/resource/loading.js"></script>
|
||||
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["src/*"],
|
||||
"~/*": ["./*"]
|
||||
},
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"allowJs": true
|
||||
},
|
||||
"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 = {
|
||||
admin: 'admin',
|
||||
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]
|
||||
138
mock/api/post.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const posts = [
|
||||
{
|
||||
title: '使用纯css优雅配置移动端rem布局',
|
||||
author: '大脸怪',
|
||||
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',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
title: '如何优雅的给图片添加水印',
|
||||
author: '大脸怪',
|
||||
category: 'JavaScript',
|
||||
description: '优雅的给图片添加水印',
|
||||
content: '我之前写过一篇文章记录了一次上传图片的优化史',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-06-24T18:46:19.000Z',
|
||||
updateDate: '2021-09-23T07:51:22.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api/posts',
|
||||
method: 'get',
|
||||
response: (data = {}) => {
|
||||
const { title, pageNo, pageSize } = data.query
|
||||
let pageData = []
|
||||
let total = 60
|
||||
const filterData = posts.filter(
|
||||
(item) => item.title.includes(title) || (!title && title !== 0)
|
||||
)
|
||||
if (filterData.length) {
|
||||
if (pageSize) {
|
||||
while (pageData.length < pageSize) {
|
||||
pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))])
|
||||
}
|
||||
} else {
|
||||
pageData = filterData
|
||||
}
|
||||
pageData = pageData.map((item, index) => ({
|
||||
id: pageSize * (pageNo - 1) + index + 1,
|
||||
...item,
|
||||
}))
|
||||
} else {
|
||||
total = 0
|
||||
}
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
pageData,
|
||||
total,
|
||||
pageNo,
|
||||
pageSize,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post',
|
||||
method: 'post',
|
||||
response: ({ body }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: body,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post/:id',
|
||||
method: 'put',
|
||||
response: ({ query, body }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: query.id,
|
||||
body,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/post/:id',
|
||||
method: 'delete',
|
||||
response: ({ query }) => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
id: query.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,24 +1,24 @@
|
||||
import { resolveToken } from '../_utils'
|
||||
import { resolveToken } from '../utils'
|
||||
|
||||
const users = {
|
||||
admin: {
|
||||
id: 1,
|
||||
name: '大脸怪(admin)',
|
||||
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||
avatar: 'https://static.isme.top/images/avatar.jpg',
|
||||
email: 'Ronnie@123.com',
|
||||
role: ['admin'],
|
||||
},
|
||||
editor: {
|
||||
id: 2,
|
||||
name: '大脸怪(editor)',
|
||||
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||
avatar: 'https://static.isme.top/images/avatar.jpg',
|
||||
email: 'Ronnie@123.com',
|
||||
role: ['editor'],
|
||||
},
|
||||
guest: {
|
||||
id: 3,
|
||||
name: '访客(guest)',
|
||||
avatar: 'https://assets.qszone.com/images/avatar.jpg',
|
||||
avatar: 'https://static.isme.top/images/avatar.jpg',
|
||||
role: [],
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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: 24,
|
||||
title: '使用jQuery的load方法帮女朋友实现套娃Html',
|
||||
author: '张传龙',
|
||||
category: 'JavaScript',
|
||||
description: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
|
||||
content: '最近女朋友刚入职新公司,接到的第一个任务就是将一个网站所有的页面合并成一个页面',
|
||||
isRecommend: true,
|
||||
isPublish: true,
|
||||
createDate: '2021-05-26T15:26:06.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',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
101
package.json
@@ -1,49 +1,82 @@
|
||||
{
|
||||
"name": "vue-naive-admin",
|
||||
"version": "0.4.0",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
"build:test": "vite build --mode test",
|
||||
"cz": "cz",
|
||||
"dev": "vite",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue .",
|
||||
"build": "vite build",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:github": "vite build --mode github && esno ./build/script",
|
||||
"lint:staged": "lint-staged",
|
||||
"prepare": "husky install",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"eslint --ext .js,.vue ."
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@zclzone",
|
||||
"@unocss",
|
||||
".eslint-global-variables.json"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^8.4.2",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.11.0",
|
||||
"md-editor-v3": "^1.11.4",
|
||||
"@unocss/eslint-config": "^0.55.7",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "5.1.12",
|
||||
"axios": "^1.5.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"md-editor-v3": "^4.7.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.0.13",
|
||||
"vue": "^3.2.31",
|
||||
"vue-router": "^4.0.15"
|
||||
"pinia": "^2.1.6",
|
||||
"vite": "^4.4.11",
|
||||
"vue": "3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.1.63",
|
||||
"@iconify/vue": "^3.2.1",
|
||||
"@vitejs/plugin-vue": "^1.10.2",
|
||||
"@vue/compiler-sfc": "^3.2.31",
|
||||
"chalk": "^5.0.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"esno": "^0.13.0",
|
||||
"fs-extra": "^10.0.1",
|
||||
"naive-ui": "^2.30.3",
|
||||
"prettier": "^2.6.1",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "^1.49.10",
|
||||
"unocss": "^0.38.2",
|
||||
"unplugin-auto-import": "^0.8.8",
|
||||
"unplugin-icons": "^0.14.1",
|
||||
"unplugin-vue-components": "^0.17.21",
|
||||
"vite": "^2.9.9",
|
||||
"@commitlint/cli": "^17.7.2",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@iconify/json": "^2.2.125",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@unocss/preset-rem-to-px": "^0.55.7",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@zclzone/eslint-config": "^0.0.5",
|
||||
"chalk": "^5.3.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-customizable": "^7.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"esno": "^0.17.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"naive-ui": "^2.35.0",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.69.0",
|
||||
"unocss": "0.55.3",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-icons": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-vue-setup-extend": "^0.3.0"
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "1.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
|
||||
7622
pnpm-lock.yaml
generated
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
endOfLine: 'lf',
|
||||
}
|
||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
@@ -10,12 +10,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-svg {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.loading-spin__container {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
@@ -45,7 +39,7 @@
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-color: var(--primaryColor);
|
||||
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;
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
/**
|
||||
* 初始化加载效果的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 = `--primaryColor: ${themeColor}`;
|
||||
document.documentElement.style.cssText = cssVars;
|
||||
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');
|
||||
addThemeColorCssVars()
|
||||
|
||||
BIN
public/resource/logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
1
settings/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './theme.json'
|
||||
37
settings/theme.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"header": {
|
||||
"height": 60
|
||||
},
|
||||
"tags": {
|
||||
"visible": true,
|
||||
"height": 50
|
||||
},
|
||||
"naiveThemeOverrides": {
|
||||
"common": {
|
||||
"primaryColor": "#316C72FF",
|
||||
"primaryColorHover": "#316C72E3",
|
||||
"primaryColorPressed": "#2B4C59FF",
|
||||
"primaryColorSuppl": "#316C72E3",
|
||||
|
||||
"infoColor": "#2080F0FF",
|
||||
"infoColorHover": "#4098FCFF",
|
||||
"infoColorPressed": "#1060C9FF",
|
||||
"infoColorSuppl": "#4098FCFF",
|
||||
|
||||
"successColor": "#18A058FF",
|
||||
"successColorHover": "#36AD6AFF",
|
||||
"successColorPressed": "#0C7A43FF",
|
||||
"successColorSuppl": "#36AD6AFF",
|
||||
|
||||
"warningColor": "#F0A020FF",
|
||||
"warningColorHover": "#FCB040FF",
|
||||
"warningColorPressed": "#C97C10FF",
|
||||
"warningColorSuppl": "#FCB040FF",
|
||||
|
||||
"errorColor": "#D03050FF",
|
||||
"errorColorHover": "#DE576DFF",
|
||||
"errorColorPressed": "#AB1F3FFF",
|
||||
"errorColorSuppl": "#DE576DFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,3 @@
|
||||
<script setup>
|
||||
import AppProvider from '@/components/common/AppProvider.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100%;
|
||||
.n-config-provider {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export const login = (data) => {
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshToken = () => {
|
||||
return request({
|
||||
url: '/auth/refreshToken',
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
6
src/api/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getPosts(data = {}) {
|
||||
return request({
|
||||
url: '/posts',
|
||||
method: 'get',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getPostById({ id }) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function savePost(id, data = {}) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/post',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePost(id) {
|
||||
return request({
|
||||
url: `/post/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defAxios as request } from '@/utils/http'
|
||||
|
||||
export function getUsers(data = {}) {
|
||||
return request({
|
||||
url: '/users',
|
||||
method: 'get',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUser(id) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
return request({
|
||||
url: '/user',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function saveUser(data = {}, id) {
|
||||
if (id) {
|
||||
return request({
|
||||
url: '/user',
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return request({
|
||||
url: `/user/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 825 B |
|
Before Width: | Height: | Size: 46 KiB |
BIN
src/assets/images/404.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 57 KiB |
BIN
src/assets/images/login_banner.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
src/assets/images/login_bg.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
23
src/components/common/AppFooter.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<footer f-c-c flex-col text-14 color="#6a6a6a">
|
||||
<p>
|
||||
Copyright © 2022-present
|
||||
<a
|
||||
href="https://github.com/zclzone"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
Ronnie Zhang
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="http://beian.miit.gov.cn/"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
赣ICP备2020015008号-2
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -1,55 +1,30 @@
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themStore.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
|
||||
wh-full
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
:theme="appStore.isDark ? darkTheme : undefined"
|
||||
:theme-overrides="naiveThemeOverrides"
|
||||
>
|
||||
<slot />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { useLoadingBar, useDialog, useMessage, useNotification } from 'naive-ui'
|
||||
|
||||
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { useThemeStore } from '@/store/modules/theme'
|
||||
import { setupMessage, setupDialog } from '@/utils/common/naiveTools'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
import { naiveThemeOverrides } from '~/settings'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const themStore = useThemeStore()
|
||||
watch(
|
||||
() => themStore.naiveThemeOverrides.common,
|
||||
(vars) => {
|
||||
for (const key in vars) {
|
||||
useCssVar(`--${key}`, document.documentElement).value = vars[key]
|
||||
if (key === 'primaryColor') {
|
||||
window.localStorage.setItem('__THEME_COLOR__', vars[key])
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 挂载naive组件的方法至window, 以便在全局使用
|
||||
function setupNaiveTools() {
|
||||
window.$loadingBar = useLoadingBar()
|
||||
window.$notification = useNotification()
|
||||
|
||||
window.$message = setupMessage(useMessage())
|
||||
window.$dialog = setupDialog(useDialog())
|
||||
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] || '')
|
||||
}
|
||||
}
|
||||
|
||||
const NaiveProviderContent = defineComponent({
|
||||
setup() {
|
||||
setupNaiveTools()
|
||||
},
|
||||
render() {
|
||||
return h('div')
|
||||
},
|
||||
})
|
||||
setupCssVar()
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<template v-if="showArrow && isOverflow">
|
||||
<div class="left" @click="handleMouseWheel({ wheelDelta: 50 })">
|
||||
<div class="left dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: 120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-left />
|
||||
</div>
|
||||
<div class="right" @click="handleMouseWheel({ wheelDelta: -50 })">
|
||||
<div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-right />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="tags-content"
|
||||
class="content"
|
||||
:class="{ overflow: isOverflow && showArrow }"
|
||||
:style="{
|
||||
height: height + 'px',
|
||||
transform: `translateX(${translateX}px)`,
|
||||
}"
|
||||
>
|
||||
@@ -24,57 +23,46 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { debounce } from '@/utils'
|
||||
import { isNullOrUndef } from '@/utils/is'
|
||||
import { debounce, useResize } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
height: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
})
|
||||
|
||||
const translateX = ref(0)
|
||||
const content = ref(null)
|
||||
const wrapper = ref(null)
|
||||
const isOverflow = ref(false)
|
||||
|
||||
function refreshIsOverflow(isIncrease) {
|
||||
isOverflow.value = content.value.offsetWidth > wrapper.value.offsetWidth
|
||||
if (isNullOrUndef(isIncrease)) return
|
||||
if (isOverflow.value) {
|
||||
handleMouseWheel({ wheelDelta: isIncrease ? -100 : 100 })
|
||||
} else if (!isIncrease && translateX.value < 0) {
|
||||
handleMouseWheel({ wheelDelta: 100 })
|
||||
}
|
||||
}
|
||||
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
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
/**
|
||||
* @wheelDelta 平行滚动的值 >0: 右移 <0: 左移
|
||||
* @translateX 内容translateX的值
|
||||
* @wrapperWidth 容器的宽度
|
||||
* @contentWidth 内容的宽度
|
||||
*/
|
||||
if (wheelDelta < 0 && -translateX.value > contentWidth - wrapperWidth + 10) {
|
||||
return
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -88,20 +76,51 @@ const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||
}
|
||||
}, 200)
|
||||
|
||||
const observers = ref([])
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
|
||||
observers.value.push(useResize(document.body, refreshIsOverflow))
|
||||
observers.value.push(useResize(content.value, refreshIsOverflow))
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
observers.value.forEach((item) => {
|
||||
item?.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
function handleScroll(x, width) {
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
if (contentWidth <= wrapperWidth) return
|
||||
|
||||
// 当 x 小于可视范围的最小值时
|
||||
if (x < -translateX.value + 150) {
|
||||
translateX.value = -(x - 150)
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
|
||||
// 当 x 大于可视范围的最大值时
|
||||
if (x + width > -translateX.value + wrapperWidth) {
|
||||
translateX.value = wrapperWidth - (x + width)
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshIsOverflow,
|
||||
handleScroll,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-wrapper {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
.tags-content {
|
||||
position: relative;
|
||||
.content {
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
22
src/components/icon/CustomIcon.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
/** 自定义图标 */
|
||||
const props = defineProps({
|
||||
/** 图标名称(assets/svg下的文件名) */
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TheIcon type="custom" v-bind="props" />
|
||||
</template>
|
||||
24
src/components/icon/SvgIcon.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
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>
|
||||
33
src/components/icon/TheIcon.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { renderIcon, renderCustomIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
/** iconify | custom */
|
||||
type: {
|
||||
type: String,
|
||||
default: 'iconify',
|
||||
},
|
||||
})
|
||||
|
||||
const iconCom = computed(() =>
|
||||
props.type === 'iconify'
|
||||
? renderIcon(props.icon, { size: props.size, color: props.color })
|
||||
: renderCustomIcon(props.icon, { size: props.size, color: props.color })
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconCom" />
|
||||
</template>
|
||||
18
src/components/page/AppPage.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
|
||||
<slot />
|
||||
<AppFooter v-if="showFooter" mt-15 />
|
||||
<n-back-top :bottom="20" />
|
||||
</section>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
33
src/components/page/CommonPage.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<AppPage :show-footer="showFooter">
|
||||
<header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<n-card flex-1 rounded-10>
|
||||
<slot />
|
||||
</n-card>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const route = useRoute()
|
||||
</script>
|
||||
27
src/components/query-bar/QueryBar.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
bg="#fafafc"
|
||||
min-h-60
|
||||
flex
|
||||
items-start
|
||||
justify-between
|
||||
b-1
|
||||
rounded-8
|
||||
p-15
|
||||
bc-ccc
|
||||
dark:bg-black
|
||||
>
|
||||
<n-space wrap :size="[35, 15]">
|
||||
<slot />
|
||||
</n-space>
|
||||
|
||||
<div flex-shrink-0>
|
||||
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
|
||||
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['search', 'reset'])
|
||||
</script>
|
||||
34
src/components/query-bar/QueryBarItem.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<label
|
||||
v-if="!isNullOrWhitespace(label)"
|
||||
w-80
|
||||
flex-shrink-0
|
||||
:style="{ width: labelWidth + 'px' }"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
contentWidth: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
55
src/components/table/CrudModal.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:style="{ width }"
|
||||
preset="card"
|
||||
:title="title"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
>
|
||||
<slot />
|
||||
<template v-if="showFooter" #footer>
|
||||
<footer flex justify-end>
|
||||
<slot name="footer">
|
||||
<n-button @click="show = false">取消</n-button>
|
||||
<n-button :loading="loading" ml-20 type="primary" @click="emit('onSave')">保存</n-button>
|
||||
</slot>
|
||||
</footer>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: '600px',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'onSave'])
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(v) {
|
||||
emit('update:visible', v)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
149
src/components/table/CrudTable.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||
<slot name="queryBar" />
|
||||
</QueryBar>
|
||||
|
||||
<n-data-table
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:scroll-x="scrollX"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:row-key="(row) => row[rowKey]"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { utils, writeFile } from 'xlsx'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @remote true: 后端分页 false: 前端分页
|
||||
*/
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @remote 是否分页
|
||||
*/
|
||||
isPagination: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
scrollX: {
|
||||
type: Number,
|
||||
default: 1200,
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
/** queryBar中的参数 */
|
||||
queryItems: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/** 补充参数(可选) */
|
||||
extraParams: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ! 约定接口入参出参
|
||||
* * 分页模式需约定分页接口入参
|
||||
* @pageSize 分页参数:一页展示多少条,默认10
|
||||
* @pageNo 分页参数:页码,默认1
|
||||
* * 需约定接口出参
|
||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||
*/
|
||||
getData: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
const loading = ref(false)
|
||||
const initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10 })
|
||||
|
||||
async function handleQuery() {
|
||||
try {
|
||||
loading.value = true
|
||||
let paginationParams = {}
|
||||
// 如果非分页模式或者使用前端分页,则无需传分页参数
|
||||
if (props.isPagination && props.remote) {
|
||||
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
|
||||
}
|
||||
const { data } = await props.getData({
|
||||
...props.queryItems,
|
||||
...props.extraParams,
|
||||
...paginationParams,
|
||||
})
|
||||
tableData.value = data?.pageData || data
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
emit('onDataChange', tableData.value)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
async function handleReset() {
|
||||
const queryItems = { ...props.queryItems }
|
||||
for (const key in queryItems) {
|
||||
queryItems[key] = ''
|
||||
}
|
||||
emit('update:queryItems', { ...queryItems, ...initQuery })
|
||||
await nextTick()
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
function onPageChange(currentPage) {
|
||||
pagination.page = currentPage
|
||||
if (props.remote) {
|
||||
handleQuery()
|
||||
}
|
||||
}
|
||||
function onChecked(rowKeys) {
|
||||
if (props.columns.some((item) => item.type === 'selection')) {
|
||||
emit('onChecked', rowKeys)
|
||||
}
|
||||
}
|
||||
function handleExport(columns = props.columns, data = tableData.value) {
|
||||
if (!data?.length) return $message.warning('没有数据')
|
||||
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
|
||||
const thKeys = columnsData.map((item) => item.key)
|
||||
const thData = columnsData.map((item) => item.title)
|
||||
const trData = data.map((item) => thKeys.map((key) => item[key]))
|
||||
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
||||
const workBook = utils.book_new()
|
||||
utils.book_append_sheet(workBook, sheet, '数据报表')
|
||||
writeFile(workBook, '数据报表.xlsx')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleExport,
|
||||
})
|
||||
</script>
|
||||
1
src/composables/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCRUD } from './useCRUD'
|
||||
103
src/composables/useCRUD.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const ACTIONS = {
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
add: '新增',
|
||||
}
|
||||
|
||||
export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||
const modalVisible = ref(false)
|
||||
const modalAction = ref('')
|
||||
const modalTitle = computed(() => ACTIONS[modalAction.value] + name)
|
||||
const modalLoading = ref(false)
|
||||
const modalFormRef = ref(null)
|
||||
const modalForm = ref({ ...initForm })
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
modalAction.value = 'add'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...initForm }
|
||||
}
|
||||
|
||||
/** 修改 */
|
||||
function handleEdit(row) {
|
||||
modalAction.value = 'edit'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 查看 */
|
||||
function handleView(row) {
|
||||
modalAction.value = 'view'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 保存 */
|
||||
function handleSave() {
|
||||
if (!['edit', 'add'].includes(modalAction.value)) {
|
||||
modalVisible.value = false
|
||||
return
|
||||
}
|
||||
modalFormRef.value?.validate(async (err) => {
|
||||
if (err) return
|
||||
const actions = {
|
||||
add: {
|
||||
api: () => doCreate(modalForm.value),
|
||||
cb: () => $message.success('新增成功'),
|
||||
},
|
||||
edit: {
|
||||
api: () => doUpdate(modalForm.value),
|
||||
cb: () => $message.success('编辑成功'),
|
||||
},
|
||||
}
|
||||
const action = actions[modalAction.value]
|
||||
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await action.api()
|
||||
action.cb()
|
||||
modalLoading.value = modalVisible.value = false
|
||||
data && refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete(id, confirmOptions) {
|
||||
if (isNullOrWhitespace(id)) return
|
||||
$dialog.confirm({
|
||||
content: '确定删除?',
|
||||
async confirm() {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await doDelete(id)
|
||||
$message.success('删除成功')
|
||||
modalLoading.value = false
|
||||
refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
},
|
||||
...confirmOptions,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
modalVisible,
|
||||
modalAction,
|
||||
modalTitle,
|
||||
modalLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleView,
|
||||
handleSave,
|
||||
modalForm,
|
||||
modalFormRef,
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveRouteNames">
|
||||
<component :is="Component" v-if="appStore.reloadFlag" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
<KeepAlive :include="keepAliveNames">
|
||||
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useTagsStore } from '@/store'
|
||||
const tagStore = useTagsStore()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
const allRoutes = router.getRoutes()
|
||||
const keepAliveRouteNames = computed(() => {
|
||||
return allRoutes.filter((route) => route.meta?.keepAlive).map((route) => route.name)
|
||||
const keepAliveNames = computed(() => {
|
||||
return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item v-for="item in route.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||
<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'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -14,4 +21,10 @@ 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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
|
||||
<icon-ant-design:fullscreen-outlined v-if="isFullscreen" />
|
||||
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
|
||||
<icon-ant-design:fullscreen-outlined v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
11
src/layout/components/header/components/GiteeSite.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<icon-simple-icons:gitee />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
function handleLinkClick() {
|
||||
window.open('https://gitee.com/isme-admin/vue-naive-admin')
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-icon mr20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<icon-mdi:github />
|
||||
</n-icon>
|
||||
</template>
|
||||
@@ -9,5 +9,3 @@ function handleLinkClick() {
|
||||
window.open('https://github.com/zclzone/vue-naive-admin')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<n-icon size="20" style="cursor: pointer" @click="appStore.switchCollapsed">
|
||||
<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'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<n-popover trigger="click" placement="bottom" @update:show="handlePopoverShow">
|
||||
<template #trigger>
|
||||
<n-badge :value="count" mr-20 cursor-pointer>
|
||||
<n-icon size="18" color-black dark="color-hex-fff">
|
||||
<icon-material-symbols:notifications-outline />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</template>
|
||||
<n-tabs v-model:value="activeTab" type="line" justify-content="space-around" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
:name="tab.name"
|
||||
:tab="tab.title + `(${tab.messages.length})`"
|
||||
>
|
||||
<ul class="cus-scroll-y max-h-200 w-220">
|
||||
<li
|
||||
v-for="(item, index) in tab.messages"
|
||||
:key="index"
|
||||
class="flex-col py-12"
|
||||
border-t="1 solid gray-200"
|
||||
:style="index > 0 ? '' : 'border: none;'"
|
||||
>
|
||||
<span mb-4 text-ellipsis>{{ item.content }}</span>
|
||||
<span text-hex-bbb>{{ item.time }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const activeTab = ref('')
|
||||
const tabs = [
|
||||
{
|
||||
name: 'zan',
|
||||
title: '点赞',
|
||||
messages: [
|
||||
{ content: '你的文章《XX》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《YY》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《AA》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《BB》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《CC》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《DD》收到一条点赞', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'star',
|
||||
title: '关注',
|
||||
messages: [
|
||||
{ content: '张三 关注了你', time: formatDateTime() },
|
||||
{ content: '王五 关注了你', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'comment',
|
||||
title: '评论',
|
||||
messages: [
|
||||
{ content: '张三 评论了你的文章《XX》"学到了"', time: formatDateTime() },
|
||||
{ content: '李四 评论了你的文章《YY》"不如Vue"', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
]
|
||||
const count = ref(tabs.map((item) => item.messages).flat().length)
|
||||
|
||||
watch(activeTab, (v) => {
|
||||
if (count === 0) return
|
||||
const tabIndex = tabs.findIndex((item) => item.name === v)
|
||||
count.value -= tabs[tabIndex].messages.length
|
||||
if (count.value < 0) count.value = 0
|
||||
})
|
||||
|
||||
function handlePopoverShow(show) {
|
||||
if (show && !activeTab.value) {
|
||||
activeTab.value = tabs[0]?.name
|
||||
}
|
||||
}
|
||||
</script>
|
||||
18
src/layout/components/header/components/ThemeMode.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
const toggleDark = () => {
|
||||
appStore.toggleDark()
|
||||
useToggle(isDark)()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-icon mr-20 cursor-pointer size="18" @click="toggleDark">
|
||||
<icon-mdi-moon-waning-crescent v-if="isDark" />
|
||||
<icon-mdi-white-balance-sunny v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div class="avatar">
|
||||
<img :src="userStore.avatar" />
|
||||
<div flex cursor-pointer items-center>
|
||||
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useUserStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -35,18 +35,3 @@ function handleSelect(key) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 100%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="h-left">
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 />
|
||||
</div>
|
||||
<div class="h-right">
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</header>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 hidden sm:block />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<MessageNotification />
|
||||
<ThemeMode />
|
||||
<GiteeSite />
|
||||
<GithubSite />
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -18,22 +19,7 @@ import MenuCollapse from './components/MenuCollapse.vue'
|
||||
import FullScreen from './components/FullScreen.vue'
|
||||
import UserAvatar from './components/UserAvatar.vue'
|
||||
import GithubSite from './components/GithubSite.vue'
|
||||
import GiteeSite from './components/GiteeSite.vue'
|
||||
import ThemeMode from './components/ThemeMode.vue'
|
||||
import MessageNotification from './components/MessageNotification.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 0 15px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.h-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.h-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<router-link h-60 f-c-c to="/">
|
||||
<icon-custom-logo text-36></icon-custom-logo>
|
||||
<h2 v-show="!appStore.collapsed" ml-10 color-primary text-16 font-bold max-w-140 flex-shrink-0>
|
||||
<img src="@/assets/images/logo.png" height="42" />
|
||||
<h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
|
||||
{{ title }}
|
||||
</h2>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
import { useAppStore } from '@/store'
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
<template>
|
||||
<n-menu
|
||||
ref="menu"
|
||||
class="side-menu"
|
||||
accordion
|
||||
:indent="18"
|
||||
:collapsed-icon-size="22"
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
:value="activeKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
import { isExternal } from '@/utils/is'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { usePermissionStore } from '@/store'
|
||||
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const curRoute = useRoute()
|
||||
const permissionStore = usePermissionStore()
|
||||
const appStore = useAppStore()
|
||||
const { currentRoute } = router
|
||||
|
||||
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.index - b.index)
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
const menu = ref(null)
|
||||
watch(curRoute, async () => {
|
||||
await nextTick()
|
||||
menu.value?.showOption()
|
||||
})
|
||||
|
||||
function resolvePath(basePath, path) {
|
||||
@@ -43,11 +48,13 @@ function getMenuItem(route, basePath = '') {
|
||||
label: (route.meta && route.meta.title) || route.name,
|
||||
key: route.name,
|
||||
path: resolvePath(basePath, route.path),
|
||||
icon: route.meta?.icon ? renderIcon(route.meta?.icon, { size: 16 }) : renderIcon('mdi:circle-outline', { size: 8 }),
|
||||
index: route.meta?.index || 0,
|
||||
icon: getIcon(route.meta),
|
||||
order: route.meta?.order || 0,
|
||||
}
|
||||
|
||||
const visibleChildren = route.children ? route.children.filter((item) => item.name && !item.isHidden) : []
|
||||
const visibleChildren = route.children
|
||||
? route.children.filter((item) => item.name && !item.isHidden)
|
||||
: []
|
||||
|
||||
if (!visibleChildren.length) return menuItem
|
||||
|
||||
@@ -55,39 +62,42 @@ function getMenuItem(route, basePath = '') {
|
||||
// 单个子路由处理
|
||||
const singleRoute = visibleChildren[0]
|
||||
menuItem = {
|
||||
...menuItem,
|
||||
label: singleRoute.meta?.title || singleRoute.name,
|
||||
key: singleRoute.name,
|
||||
path: resolvePath(menuItem.path, singleRoute.path),
|
||||
icon: singleRoute.meta?.icon
|
||||
? renderIcon(singleRoute.meta?.icon, { size: 16 })
|
||||
: renderIcon('mdi:circle-outline', { size: 8 }),
|
||||
index: menuItem.index,
|
||||
icon: getIcon(singleRoute.meta),
|
||||
}
|
||||
const visibleItems = singleRoute.children ? singleRoute.children.filter((item) => item.name && !item.isHidden) : []
|
||||
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.index - b.index)
|
||||
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.index - b.index)
|
||||
.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)
|
||||
}
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -102,7 +112,7 @@ function handleMenuSelect(key, item) {
|
||||
&.n-menu-item-content--selected,
|
||||
&:hover {
|
||||
&::before {
|
||||
border-left: 4px solid var(--primaryColor);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-dropdown
|
||||
:show="dropdownShow"
|
||||
:show="show"
|
||||
:options="options"
|
||||
:x="x"
|
||||
:y="y"
|
||||
@@ -11,9 +11,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import { renderIcon } from '@/utils/icon'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useTagsStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -37,7 +36,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const tagsStore = useTagsStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
@@ -67,25 +65,19 @@ const options = computed(() => [
|
||||
{
|
||||
label: '关闭右侧',
|
||||
key: 'close-right',
|
||||
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
|
||||
disabled:
|
||||
tagsStore.tags.length <= 1 ||
|
||||
props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
|
||||
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
|
||||
},
|
||||
])
|
||||
|
||||
const dropdownShow = computed({
|
||||
get() {
|
||||
return props.show
|
||||
},
|
||||
set(show) {
|
||||
emit('update:show', show)
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
() => {
|
||||
appStore.reloadPage()
|
||||
tagsStore.reloadTag(route.path, route.meta?.keepAlive)
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -115,7 +107,7 @@ const actionMap = new Map([
|
||||
])
|
||||
|
||||
function handleHideDropdown() {
|
||||
dropdownShow.value = false
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function handleSelect(key) {
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
<template>
|
||||
<ScrollX ref="scrollX" :height="useTheme.tags.height">
|
||||
<ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
|
||||
<n-tag
|
||||
v-for="tag in tagsStore.tags"
|
||||
ref="tabRefs"
|
||||
:key="tag.path"
|
||||
class="mx-5 cursor-pointer rounded-4 px-15 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)"
|
||||
>
|
||||
<template v-if="tag.icon" #icon>
|
||||
<TheIcon :icon="tag.icon" class="mr-4" />
|
||||
</template>
|
||||
{{ 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>
|
||||
|
||||
<ContextMenu
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup name="Tags">
|
||||
<script setup>
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTagsStore } from '@/store/modules/tags'
|
||||
import { useThemeStore } from '@/store/modules/theme'
|
||||
import { useTagsStore } from '@/store'
|
||||
import ScrollX from '@/components/common/ScrollX.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tagsStore = useTagsStore()
|
||||
const useTheme = useThemeStore()
|
||||
const tabRefs = ref([])
|
||||
const scrollXRef = ref(null)
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
show: false,
|
||||
@@ -42,20 +47,25 @@ const contextMenuOption = reactive({
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
const { name, path } = route
|
||||
const { name, fullPath: path } = route
|
||||
const title = route.meta?.title
|
||||
tagsStore.addTag({ name, path, title })
|
||||
const icon = route.meta?.icon
|
||||
const keepAlive = route.meta?.keepAlive
|
||||
tagsStore.addTag({ name, path, title, icon, keepAlive })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const scrollX = ref(null)
|
||||
watch(
|
||||
() => tagsStore.tags,
|
||||
async (newVal, oldVal) => {
|
||||
() => tagsStore.activeIndex,
|
||||
async (activeIndex) => {
|
||||
await nextTick()
|
||||
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
|
||||
}
|
||||
const activeTabElement = tabRefs.value[activeIndex]?.$el
|
||||
if (!activeTabElement) return
|
||||
const { offsetLeft: x, offsetWidth: width } = activeTabElement
|
||||
scrollXRef.value?.handleScroll(x + width, width)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleTagClick = (path) => {
|
||||
@@ -83,25 +93,14 @@ async function handleContextMenu(e, tagItem) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.n-tag {
|
||||
padding: 0 15px;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
.n-tag__close {
|
||||
margin-left: 5px;
|
||||
box-sizing: content-box;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.7s;
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
<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,5 +1,5 @@
|
||||
<template>
|
||||
<n-layout has-sider style="height: 100%">
|
||||
<n-layout has-sider wh-full>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
@@ -10,22 +10,23 @@
|
||||
>
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header :style="{ height: useTheme.header.height + 'px' }">
|
||||
<AppHeader />
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout style="background-color: #f5f6fb" :style="`height: calc(100% - ${useTheme.header.height}px)`">
|
||||
<AppTags v-if="useTheme.tags.visible" />
|
||||
<AppMain
|
||||
class="cur-scroll border-t bc-eee"
|
||||
:style="{
|
||||
height: `calc(100% - ${useTheme.tags.visible ? useTheme.tags.height : 0}px)`,
|
||||
overflow: 'auto',
|
||||
}"
|
||||
/>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
<article flex-col flex-1 overflow-hidden>
|
||||
<header
|
||||
border-b="1 solid #eee"
|
||||
class="flex items-center bg-white px-15"
|
||||
dark="bg-dark border-0"
|
||||
:style="`height: ${header.height}px`"
|
||||
>
|
||||
<AppHeader />
|
||||
</header>
|
||||
<section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
|
||||
<AppTags :style="{ height: `${tags.height}px` }" />
|
||||
</section>
|
||||
<section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
|
||||
<AppMain />
|
||||
</section>
|
||||
</article>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
@@ -34,17 +35,8 @@ 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 { useThemeStore } from '@/store/modules/theme'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAppStore } from '@/store'
|
||||
import { header, tags } from '~/settings'
|
||||
|
||||
const useTheme = useThemeStore()
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.n-layout-header {
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
|
||||
14
src/main.js
@@ -1,21 +1,21 @@
|
||||
/** 重置样式 */
|
||||
import '@/styles/reset.css'
|
||||
import '@/styles/variables.css'
|
||||
import '@/styles/index.scss'
|
||||
import 'uno.css'
|
||||
import '@/styles/global.scss'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { setupRouter } from '@/router'
|
||||
import { setupStore } from '@/store'
|
||||
import App from './App.vue'
|
||||
import { setupNaiveDiscreteApi } from './utils'
|
||||
|
||||
function setupApp() {
|
||||
async function setupApp() {
|
||||
const app = createApp(App)
|
||||
|
||||
setupStore(app)
|
||||
|
||||
setupRouter(app)
|
||||
|
||||
await setupRouter(app)
|
||||
app.mount('#app')
|
||||
setupNaiveDiscreteApi()
|
||||
}
|
||||
|
||||
setupApp()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const baseTitle = import.meta.env.VITE_APP_TITLE
|
||||
const baseTitle = import.meta.env.VITE_TITLE
|
||||
|
||||
export function createPageTitleGuard(router) {
|
||||
router.afterEach((to) => {
|
||||
|
||||
@@ -1,44 +1,20 @@
|
||||
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 { getToken, refreshAccessToken, isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const WHITE_LIST = ['/login', '/redirect']
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
if (userStore.userId) {
|
||||
// 已经拿到用户信息
|
||||
refreshAccessToken()
|
||||
next()
|
||||
} else {
|
||||
await userStore.getUserInfo().catch((error) => {
|
||||
removeToken()
|
||||
toLogin()
|
||||
$message.error(error.message || '获取用户信息失败!')
|
||||
return
|
||||
})
|
||||
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 })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (WHITE_LIST.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
next({ path: '/login', query: { ...to.query, redirect: to.path } })
|
||||
}
|
||||
|
||||
/** 没有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: '/' }
|
||||
|
||||
refreshAccessToken()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,28 +1,83 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes } from './routes'
|
||||
import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
|
||||
import { getToken, isNullOrWhitespace } from '@/utils'
|
||||
import { useUserStore, usePermissionStore } from '@/store'
|
||||
|
||||
const isHash = !!import.meta.env.VITE_APP_USE_HASH
|
||||
const isHash = import.meta.env.VITE_USE_HASH === 'true'
|
||||
export const router = createRouter({
|
||||
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
routes: [],
|
||||
routes: basicRoutes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
export function resetRouter() {
|
||||
export async function setupRouter(app) {
|
||||
await addDynamicRoutes()
|
||||
setupRouterGuard(router)
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
export async function resetRouter() {
|
||||
const basicRouteNames = getRouteNames(basicRoutes)
|
||||
router.getRoutes().forEach((route) => {
|
||||
const { name } = route
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
})
|
||||
basicRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
const name = route.name
|
||||
if (!basicRouteNames.includes(name)) {
|
||||
router.removeRoute(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setupRouter(app) {
|
||||
basicRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
app.use(router)
|
||||
setupRouterGuard(router)
|
||||
export async function addDynamicRoutes() {
|
||||
// return Promise.reject('123')
|
||||
const token = getToken()
|
||||
|
||||
// 没有token情况
|
||||
if (isNullOrWhitespace(token)) {
|
||||
router.addRoute(EMPTY_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
// 有token的情况
|
||||
const userStore = useUserStore()
|
||||
try {
|
||||
const permissionStore = usePermissionStore()
|
||||
!userStore.userId && (await userStore.getUserInfo())
|
||||
const accessRoutes = permissionStore.generateRoutes(userStore.role)
|
||||
accessRoutes.forEach((route) => {
|
||||
!router.hasRoute(route.name) && router.addRoute(route)
|
||||
})
|
||||
router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name)
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
|
||||
window.$notification?.success({
|
||||
title: '🎉🎉🎉 2.0 全栈版本开源了!',
|
||||
content: () =>
|
||||
h(
|
||||
'span',
|
||||
{},
|
||||
'2.0为全栈版本,提供前端+后端,全新重构,全面简化,',
|
||||
h(
|
||||
'a',
|
||||
{ href: 'https://admin.isme.top', target: '__blank' },
|
||||
'👉体验 https://admin.isme.top'
|
||||
)
|
||||
),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$message.error('初始化用户信息失败: ' + error)
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
|
||||
export function getRouteNames(routes) {
|
||||
return routes.map((route) => getRouteName(route)).flat(1)
|
||||
}
|
||||
|
||||
function getRouteName(route) {
|
||||
const names = [route.name]
|
||||
if (route.children && route.children.length) {
|
||||
names.push(...route.children.map((item) => getRouteName(item)).flat(1))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
import Home from '@/views/dashboard/index.vue'
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export const basicRoutes = [
|
||||
{
|
||||
@@ -8,21 +7,9 @@ export const basicRoutes = [
|
||||
component: () => import('@/views/error-page/404.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',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
isHidden: true,
|
||||
@@ -31,97 +18,6 @@ export const basicRoutes = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/home',
|
||||
meta: {
|
||||
title: 'Dashboard',
|
||||
icon: 'mdi:chart-bar',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Home',
|
||||
path: 'home',
|
||||
component: Home,
|
||||
meta: {
|
||||
title: '首页',
|
||||
icon: 'mdi:home',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ErrorPage',
|
||||
path: '/error-page',
|
||||
component: Layout,
|
||||
redirect: '/error-page/404',
|
||||
meta: {
|
||||
title: '错误页',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
index: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ERROR-404',
|
||||
path: '404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Test',
|
||||
path: '/test',
|
||||
component: Layout,
|
||||
redirect: '/test/unocss',
|
||||
meta: {
|
||||
title: '基础功能测试',
|
||||
icon: 'mdi:menu',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Unocss',
|
||||
path: 'unocss',
|
||||
component: () => import('@/views/test-page/unocss/index.vue'),
|
||||
meta: {
|
||||
title: '测试unocss',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
path: 'message',
|
||||
component: () => import('@/views/test-page/message/index.vue'),
|
||||
meta: {
|
||||
title: '测试Message',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dialog',
|
||||
path: 'dialog',
|
||||
component: () => import('@/views/test-page/dialog/index.vue'),
|
||||
meta: {
|
||||
title: '测试Dialog',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TestKeepAlive',
|
||||
path: 'keep-alive',
|
||||
component: () => import('@/views/test-page/keep-alive/index.vue'),
|
||||
meta: {
|
||||
title: '测试Keep-Alive',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ExternalLink',
|
||||
path: '/external-link',
|
||||
@@ -129,6 +25,7 @@ export const basicRoutes = [
|
||||
meta: {
|
||||
title: '外部链接',
|
||||
icon: 'mdi:link-variant',
|
||||
order: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -166,10 +63,16 @@ export const NOT_FOUND_ROUTE = {
|
||||
isHidden: true,
|
||||
}
|
||||
|
||||
const modules = import.meta.globEager('./modules/*.js')
|
||||
export const EMPTY_ROUTE = {
|
||||
name: 'Empty',
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: null,
|
||||
}
|
||||
|
||||
const modules = import.meta.glob('@/views/**/route.js', { eager: true })
|
||||
const asyncRoutes = []
|
||||
Object.keys(modules).forEach((key) => {
|
||||
asyncRoutes.push(...modules[key].default)
|
||||
asyncRoutes.push(modules[key].default)
|
||||
})
|
||||
|
||||
export { asyncRoutes }
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Example',
|
||||
path: '/example',
|
||||
component: Layout,
|
||||
redirect: '/example/table',
|
||||
meta: {
|
||||
title: '组件示例',
|
||||
role: ['admin'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Table',
|
||||
path: 'table',
|
||||
component: () => import('@/views/examples/table/index.vue'),
|
||||
redirect: '/example/table/post',
|
||||
meta: {
|
||||
title: '表格',
|
||||
role: ['admin'],
|
||||
icon: 'mdi:table',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'PostList',
|
||||
path: 'post',
|
||||
component: () => import('@/views/examples/table/post/index.vue'),
|
||||
meta: {
|
||||
title: '文章列表',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PostCreate',
|
||||
path: 'post-create',
|
||||
component: () => import('@/views/examples/table/post/PostCreate.vue'),
|
||||
meta: {
|
||||
title: '创建文章',
|
||||
role: ['admin'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
export { default as themeSettings } from './theme.json'
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"tags": {
|
||||
"visible": true,
|
||||
"height": 50
|
||||
},
|
||||
"header": {
|
||||
"height": 60
|
||||
},
|
||||
"naiveThemeOverrides": {
|
||||
"common": {
|
||||
"primaryColor": "#316C72FF",
|
||||
"primaryColorHover": "#316C72E3",
|
||||
"primaryColorPressed": "#2B4C59FF",
|
||||
"primaryColorSuppl": "#316C7263"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,5 @@ import { createPinia } from 'pinia'
|
||||
export function setupStore(app) {
|
||||
app.use(createPinia())
|
||||
}
|
||||
|
||||
export * from './modules'
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
export const useAppStore = defineStore('app', {
|
||||
state() {
|
||||
return {
|
||||
reloadFlag: true,
|
||||
collapsed: false,
|
||||
isDark,
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
/** 设置暗黑模式 */
|
||||
setDark(isDark) {
|
||||
this.isDark = isDark
|
||||
},
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDark() {
|
||||
this.isDark = !this.isDark
|
||||
},
|
||||
},
|
||||
})
|
||||