Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b7f93d97 | ||
|
|
7f455c5494 | ||
|
|
382ece82f7 | ||
|
|
3bd695627d | ||
|
|
ceb6a1f5ba | ||
|
|
88900d6ed3 | ||
|
|
d40c42a810 | ||
|
|
91c7350c34 | ||
|
|
924a42e270 | ||
|
|
5fcfcb7770 | ||
|
|
65b2b0fcda | ||
|
|
c34c2d1052 | ||
|
|
159832443a | ||
|
|
0ae0f6f352 | ||
|
|
0f19e26650 | ||
|
|
efd8536469 | ||
|
|
3ed947c8da | ||
|
|
ffd040a0ef | ||
|
|
026dfdbaef | ||
|
|
4c22d91170 | ||
|
|
6327abb078 | ||
|
|
484f35d27a | ||
|
|
a5b3c5660d | ||
|
|
76c66f9bc9 | ||
|
|
b3cfb95f59 | ||
|
|
dac7f4b0c1 | ||
|
|
d83b21715b | ||
|
|
a57c62765b | ||
|
|
2b38152485 | ||
|
|
57e982ed61 | ||
|
|
2fe0fc95ee | ||
|
|
fab5d0539b | ||
|
|
e79bd1dc50 | ||
|
|
1e155369b2 | ||
|
|
cb230f77bd | ||
|
|
6f59ddf6b8 | ||
|
|
7668bd5063 | ||
|
|
5e3fa9a567 | ||
|
|
571190cd78 | ||
|
|
bd5c921bca | ||
|
|
b236df4e30 | ||
|
|
657bec97f6 | ||
|
|
5d0141011b | ||
|
|
b8e53fb259 | ||
|
|
66c0e4f8c7 | ||
|
|
7457fcc23e | ||
|
|
9a7331f32f | ||
|
|
87819b4b5d | ||
|
|
d00fc26616 | ||
|
|
647edd7207 | ||
|
|
23d36432da | ||
|
|
2fdc4f18ca | ||
|
|
961df08e2b | ||
|
|
1d62ac7558 | ||
|
|
456ef83081 | ||
|
|
90b588028a | ||
|
|
9e444c83f8 | ||
|
|
57fcd2785c | ||
|
|
3c8b4881b4 | ||
|
|
e32d0561c7 | ||
|
|
d303776825 | ||
|
|
3ef0f69bcc | ||
|
|
b00477d5fa | ||
|
|
fe00fa1e6f | ||
|
|
f0679a1d59 | ||
|
|
4c4f811f39 | ||
|
|
03e5e99896 | ||
|
|
fda48176ee | ||
|
|
b639b487db | ||
|
|
dbd93ffb7f | ||
|
|
daf9b1b2e2 | ||
|
|
e023671bac | ||
|
|
e6265a7c64 | ||
|
|
8c629050ba | ||
|
|
c7d2a857f7 | ||
|
|
b19344a1c1 | ||
|
|
b632b4d3a8 | ||
|
|
98d21845ce | ||
|
|
bbeeac41ce | ||
|
|
449495f03d | ||
|
|
14583bbaa5 | ||
|
|
045ab02bb2 | ||
|
|
b883133dc6 | ||
|
|
5269dba56b | ||
|
|
4911affd28 | ||
|
|
cb53f4c84e | ||
|
|
3164e33f1d | ||
|
|
06eee21032 | ||
|
|
79184f4eeb | ||
|
|
8416b61f6b | ||
|
|
a90a19c62e | ||
|
|
ee819445d2 | ||
|
|
679ad4c9ee | ||
|
|
805c13ba7a | ||
|
|
c61675b311 | ||
|
|
a105df66b1 | ||
|
|
107e820776 | ||
|
|
04fa39c295 | ||
|
|
1685f8b7be | ||
|
|
73074101e1 | ||
|
|
e802354cab | ||
|
|
26274fdb8e | ||
|
|
f26b4cc50e | ||
|
|
fd1d57cebe | ||
|
|
4ac539d082 | ||
|
|
fa81882f25 | ||
|
|
606ce62cfd | ||
|
|
7479dfe818 | ||
|
|
a8e964acd4 | ||
|
|
aab429b997 | ||
|
|
64fd2eb59c | ||
|
|
c9d25a0e8d | ||
|
|
fc44d941d0 | ||
|
|
362562112a | ||
|
|
5e5100de11 | ||
|
|
b60236035a | ||
|
|
a4b33eea73 | ||
|
|
46138b8da9 | ||
|
|
b4e0e9740d | ||
|
|
af1324c2cf | ||
|
|
a8b41c7237 | ||
|
|
5704453835 | ||
|
|
f9c1c2df42 | ||
|
|
9812058805 | ||
|
|
a55f3016ce | ||
|
|
52b25d1eae | ||
|
|
42da9626b7 | ||
|
|
06d6e18dac | ||
|
|
55dcff3b22 | ||
|
|
8d5c14ea16 | ||
|
|
36c9a73375 | ||
|
|
f6477e03db | ||
|
|
0dfdb0ff32 | ||
|
|
2f1dc07eff | ||
|
|
af49dc8113 | ||
|
|
67e16872e4 | ||
|
|
1dd1677969 | ||
|
|
6c1fca69dc | ||
|
|
f3993b8563 | ||
|
|
d39f5c3af6 | ||
|
|
86df38a142 | ||
|
|
548eac59f2 | ||
|
|
8dee916774 | ||
|
|
59ffda20e7 | ||
|
|
d908f3ce26 | ||
|
|
cc9226f19e | ||
|
|
30a303195f | ||
|
|
75d815d315 | ||
|
|
2b8a198aaf | ||
|
|
e015f29e9e | ||
|
|
795d428ed8 | ||
|
|
8adcb2654b | ||
|
|
fbef475459 | ||
|
|
8311afa712 | ||
|
|
dc17038185 | ||
|
|
cf63b0c0f6 | ||
|
|
b9615a49e7 | ||
|
|
28b8df7c77 | ||
|
|
ffc402d27f | ||
|
|
ccc8e51ebd | ||
|
|
6e1aacab06 | ||
|
|
55edebae61 | ||
|
|
0fffdebb89 | ||
|
|
aab8f77d17 | ||
|
|
e5b32aed8b | ||
|
|
031e6f9c11 | ||
|
|
671520456f | ||
|
|
2e98278f74 | ||
|
|
b5b5a8e873 | ||
|
|
b96e9dc4c9 | ||
|
|
61eded401e | ||
|
|
0be837ce7e | ||
|
|
e57331905f | ||
|
|
98e85220d1 | ||
|
|
7ba47879f7 | ||
|
|
45567dddbc | ||
|
|
0aa24229dd | ||
|
|
3e313f4d66 | ||
|
|
fc5cd48125 | ||
|
|
8aba058479 | ||
|
|
4df198c4b3 | ||
|
|
04ea92c714 | ||
|
|
7cef77df24 | ||
|
|
6abc4b696c | ||
|
|
d9c14872ab | ||
|
|
8a37439237 | ||
|
|
eac185559a | ||
|
|
be8041378b | ||
|
|
2e4af7c2f5 | ||
|
|
3c3fbd1ece | ||
|
|
197d302d1b | ||
|
|
ff2dd31096 | ||
|
|
0283bfc5dd | ||
|
|
95748fe4a9 | ||
|
|
8e1abdb5c2 | ||
|
|
00c5a1727b | ||
|
|
05b7618536 | ||
|
|
9a30f99797 | ||
|
|
96dae4047b | ||
|
|
33294c516e |
@@ -1,6 +1,5 @@
|
||||
// .commitlintrc.js
|
||||
/** @type {import('cz-git').UserConfig} */
|
||||
module.exports = {
|
||||
export default {
|
||||
rules: {
|
||||
// @see: https://commitlint.js.org/#/reference-rules
|
||||
},
|
||||
@@ -16,7 +15,7 @@ module.exports = {
|
||||
footerPrefixsSelect: '选择关联issue前缀(可选):',
|
||||
customFooterPrefixs: '输入自定义issue前缀 :',
|
||||
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
|
||||
confirmCommit: '是否提交或修改commit ?'
|
||||
confirmCommit: '是否提交或修改commit ?',
|
||||
},
|
||||
types: [
|
||||
{ value: 'feat', name: 'feat: ✨ 新增功能 | A new feature', emoji: ':sparkles:' },
|
||||
@@ -49,7 +48,7 @@ module.exports = {
|
||||
issuePrefixs: [
|
||||
// 如果使用 gitee 作为开发管理
|
||||
{ value: 'link', name: 'link: 链接 ISSUES 进行中' },
|
||||
{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' }
|
||||
{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' },
|
||||
],
|
||||
customIssuePrefixsAlign: 'top',
|
||||
emptyIssuePrefixsAlias: 'skip',
|
||||
@@ -57,13 +56,13 @@ module.exports = {
|
||||
allowCustomIssuePrefixs: true,
|
||||
allowEmptyIssuePrefixs: true,
|
||||
confirmColorize: true,
|
||||
maxHeaderLength: Infinity,
|
||||
maxSubjectLength: Infinity,
|
||||
maxHeaderLength: Number.POSITIVE_INFINITY,
|
||||
maxSubjectLength: Number.POSITIVE_INFINITY,
|
||||
minSubjectLength: 0,
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: '',
|
||||
defaultIssues: '',
|
||||
defaultScope: '',
|
||||
defaultSubject: ''
|
||||
}
|
||||
defaultSubject: '',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = true
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = Fantastic-admin 基础版
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
VITE_APP_DEBUG_TOOL =
|
||||
# 是否禁用开发者工具,可防止被调试
|
||||
VITE_APP_DISABLE_DEVTOOL = false
|
||||
|
||||
# 是否开启代理
|
||||
VITE_OPEN_PROXY = false
|
||||
# 是否开启开发者工具
|
||||
VITE_OPEN_DEVTOOLS = false
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = false
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 页面标题
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
VITE_APP_DEBUG_TOOL =
|
||||
# 是否禁用开发者工具,可防止被调试
|
||||
VITE_APP_DISABLE_DEVTOOL = false
|
||||
|
||||
# 是否在打包时启用 Mock
|
||||
VITE_BUILD_MOCK = false
|
||||
@@ -11,3 +15,5 @@ VITE_BUILD_MOCK = false
|
||||
VITE_BUILD_SOURCEMAP = false
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip,brotli
|
||||
# 是否在打包后生成存档,支持 zip 和 tar
|
||||
VITE_BUILD_ARCHIVE =
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = false
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 页面标题(test)
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
VITE_APP_DEBUG_TOOL =
|
||||
# 是否禁用开发者工具,可防止被调试
|
||||
VITE_APP_DISABLE_DEVTOOL = false
|
||||
|
||||
# 是否在打包时启用 Mock
|
||||
VITE_BUILD_MOCK = true
|
||||
@@ -11,3 +15,5 @@ VITE_BUILD_MOCK = true
|
||||
VITE_BUILD_SOURCEMAP = true
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS =
|
||||
# 是否在打包后生成存档,支持 zip 和 tar
|
||||
VITE_BUILD_ARCHIVE =
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "@antfu",
|
||||
"rules": {
|
||||
"eslint-comments/no-unlimited-disable": "off",
|
||||
"curly": ["error", "all"],
|
||||
"vue/component-tags-order": ["error", {
|
||||
"order": ["route", "script", "template", "style"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 22
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Create Archive
|
||||
uses: thedoctor0/zip-release@main
|
||||
with:
|
||||
type: 'zip'
|
||||
type: zip
|
||||
filename: fantastic-admin.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
exclusions: '/.git/* /.github/*'
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: example
|
||||
@@ -73,9 +73,9 @@ jobs:
|
||||
- name: Create Archive
|
||||
uses: thedoctor0/zip-release@main
|
||||
with:
|
||||
type: 'zip'
|
||||
type: zip
|
||||
filename: fantastic-admin.example.${{ steps.last_release.outputs.tag_name }}.zip
|
||||
exclusions: '/.git/* /.github/*'
|
||||
exclusions: '/.git/* /.github/* /public/.github/* /public/.nojekyll'
|
||||
|
||||
- name: Upload Archive To Release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: repo-sync
|
||||
|
||||
on:
|
||||
# 每天自动同步
|
||||
# 每天 00:05 自动同步
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '5 0 * * *'
|
||||
# 手动触发部署
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -5,6 +5,3 @@ dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
src/assets/sprites/*.*
|
||||
!src/assets/sprites/.gitkeep
|
||||
public/icons
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"*.{ts,tsx,vue}": "eslint --cache --fix --no-ignore --no-error-on-unmatched-pattern --ext .ts,.tsx,.vue src/",
|
||||
"*.{css,scss,vue}": "stylelint src/**/*.{css,scss,vue} --cache --fix --allow-empty-input"
|
||||
"*.{ts,tsx,vue}": "eslint --cache --fix",
|
||||
"*.{css,scss,vue}": "stylelint --cache --fix"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
22
|
||||
@@ -1,2 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
engine-strict=true
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
src/assets/sprites
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-stylistic/config",
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-standard-vue/scss"
|
||||
],
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"stylistic/max-line-length": null,
|
||||
"stylistic/block-closing-brace-newline-after": [
|
||||
"always",
|
||||
{
|
||||
"ignoreAtRules": ["if", "else"]
|
||||
}
|
||||
],
|
||||
"at-rule-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"property-no-unknown": null,
|
||||
"font-family-no-missing-generic-family-keyword": null,
|
||||
"selector-class-pattern": null,
|
||||
"scss/double-slash-comment-empty-line-before": null,
|
||||
"scss/no-global-function-names": null
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"antfu.unocss"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
{
|
||||
"eslint.useFlatConfig": true,
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"stylelint.validate": ["css", "scss", "vue"]
|
||||
"stylelint.validate": [
|
||||
"css",
|
||||
"postcss",
|
||||
"scss",
|
||||
"vue"
|
||||
],
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 fantastic-template
|
||||
Copyright (c) 2020 fantastic-admin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
<p align="center">
|
||||
<img src="https://fantastic-admin.gitee.io/logo.png" width="200" height="200" />
|
||||
<img src="https://fantastic-admin.hurui.me/logo.png" width="200" height="200" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Fantastic-admin</h1>
|
||||
|
||||
<p align="center">一款<b>开箱即用</b>的 Vue 中后台管理系统框架</p>
|
||||
<p align="center">一款<b>开箱即用</b>的 Vue3 中后台管理系统框架</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fantastic-admin.gitee.io" target="_blank">官网</a>
|
||||
( <a href="https://fantastic-admin.github.io" target="_blank">备用</a> )
|
||||
<a href="https://fantastic-admin.hurui.me" target="_blank">官网</a>
|
||||
<span> | </span>
|
||||
演示
|
||||
( <a href="https://fantastic-admin.gitee.io/basic-example" target="_blank">基础版</a>
|
||||
<a href="https://fantastic-admin.gitee.io/pro-example" target="_blank">专业版</a> )
|
||||
<span> | </span>
|
||||
<a href="https://fantastic-admin.gitee.io/support.html" target="_blank">技术支持</a>
|
||||
<span> | </span>
|
||||
<a href="https://fantastic-admin.gitee.io/buy.html" target="_blank">购买</a>
|
||||
<a href="https://fantastic-admin.pages.dev" target="_blank">备用地址</a>
|
||||
<p>
|
||||
|
||||
<p align="center">
|
||||
@@ -26,9 +19,9 @@
|
||||
|
||||
## 特点
|
||||
|
||||
- 可自由替换 UI 组件库,默认使用 Element Plus
|
||||
- 丰富的布局与主题,覆盖市面上各种中后台应用场景,兼容PC、平板和移动端
|
||||
- 提供系统配置文件,轻松实现个性化定制
|
||||
- 精心设计的动效,让每一处的动画都干净利落
|
||||
- 根据路由配置自动生成导航栏
|
||||
- 基于文件系统的路由
|
||||
- 支持全方位权限验证
|
||||
@@ -36,29 +29,31 @@
|
||||
- 轻松实现国际化多语言适配
|
||||
- 提供接近于浏览器原生标签栏操作体验的标签页功能
|
||||
|
||||
## 分支说明
|
||||
## 下载
|
||||
|
||||
> 本仓库为基础版
|
||||
|
||||
**直接拉取源码可能会包含未发布的内容,推荐去 [Github Releases](https://github.com/fantastic-admin/basic/releases) 页面下载稳定版本的压缩包**。如果确定需要拉取源码,请参考下列分支说明:
|
||||
|
||||
- `main` Vue3 版本框架源码分支,不含示例代码,可直接用于实际开发
|
||||
- `example` Vue3 版本演示源码分支,同线上演示站,包含大量示例,可用于参考学习
|
||||
- `vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发(停止维护)
|
||||
- `vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习(停止维护)
|
||||
|
||||
如果你更注重使用时的稳定性,推荐去 [Github Releases](https://github.com/fantastic-admin/basic/releases) 页面下载最新稳定版本的压缩包。
|
||||
|
||||
## 预览
|
||||
|
||||
> 预览截图为 Vue3 专业版
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview1.png" /></td>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview2.png" /></td>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview3.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview3.png" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview4.png" /></td>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview5.png" /></td>
|
||||
<td><img src="https://fantastic-admin.gitee.io/preview6.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview4.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview5.png" /></td>
|
||||
<td><img src="https://fantastic-admin.hurui.me/preview6.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -70,17 +65,23 @@
|
||||
|
||||
[](https://gitee.com/fantastic-admin/basic)
|
||||
|
||||
<details>
|
||||
<summary>Github Stars 曲线</summary>
|
||||
|
||||
[](https://starchart.cc/fantastic-admin/basic)
|
||||
</details>
|
||||
|
||||
## 生态
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
<a href="https://hooray.gitee.io/fantastic-startkit/" target="_blank">Fantastic-startkit</a>
|
||||
<a href="https://hooray.github.io/fantastic-startkit/" target="_blank">Fantastic-startkit</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
一款开箱即用的 Vue 项目启动套件
|
||||
一款简单好用的 Vue3 项目启动套件
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -88,7 +89,7 @@
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="3" align="center">
|
||||
<a href="https://one-step-admin.gitee.io" target="_blank">One-step-admin</a>
|
||||
<a href="https://one-step-admin.hurui.me" target="_blank">One-step-admin</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -97,13 +98,32 @@
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview1.png" /></td>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview2.png" /></td>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview3.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview3.png" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview4.png" /></td>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview5.png" /></td>
|
||||
<td><img src="https://one-step-admin.gitee.io/preview6.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview4.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview5.png" /></td>
|
||||
<td><img src="https://one-step-admin.hurui.me/preview6.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="4" align="center">
|
||||
<a href="https://fantastic-mobile.hurui.me" target="_blank">Fantastic-mobile</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" align="center">
|
||||
一款自成一派的移动端 H5 框架
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview1.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview2.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview3.png" /></td>
|
||||
<td><img src="https://fantastic-mobile.hurui.me/preview4.png" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/assets/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"framework": "vite",
|
||||
"aliases": {
|
||||
"components": "@/ui/shadcn",
|
||||
"utils": "@/utils"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu(
|
||||
{
|
||||
unocss: true,
|
||||
ignores: [
|
||||
'public',
|
||||
'dist*',
|
||||
'src/ui/shadcn/ui',
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'curly': ['error', 'all'],
|
||||
'ts/no-unused-expressions': ['error', {
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/**/*.vue',
|
||||
],
|
||||
rules: {
|
||||
'vue/block-order': ['error', {
|
||||
order: ['route', 'script', 'template', 'style'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/loading.css" />
|
||||
<link rel="stylesheet" href="/browser_upgrade/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Expires" content="0">
|
||||
@@ -14,21 +13,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="fantastic-admin-home">
|
||||
<div class="loading">
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
</div>
|
||||
<div class="text">载入中</div>
|
||||
</div>
|
||||
<div id="browser-upgrade">
|
||||
<div class="title">为了您的体验,推荐使用以下浏览器</div>
|
||||
<div class="browsers">
|
||||
<a href="https://www.microsoft.com/edge" target="_blank" class="browser">
|
||||
<img class="browser-icon" src="/browser_upgrade/edge.png" />
|
||||
<div class="browser-name">Mircosoft Edge</div>
|
||||
<div class="browser-name">Microsoft Edge</div>
|
||||
</a>
|
||||
<a href="https://www.google.cn/chrome/" target="_blank" class="browser">
|
||||
<img class="browser-icon" src="/browser_upgrade/chrome.png" />
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
<style>
|
||||
@keyframes rainbow {
|
||||
0% { --rainbow-color: #00a98e; }
|
||||
1.25% { --rainbow-color: #00a996; }
|
||||
2.5% { --rainbow-color: #00a99f; }
|
||||
3.75% { --rainbow-color: #00a9a7; }
|
||||
5% { --rainbow-color: #00a9b0; }
|
||||
6.25% { --rainbow-color: #00a9b8; }
|
||||
7.5% { --rainbow-color: #00a9c0; }
|
||||
8.75% { --rainbow-color: #00a8c7; }
|
||||
10% { --rainbow-color: #00a8cf; }
|
||||
11.25% { --rainbow-color: #00a7d5; }
|
||||
12.5% { --rainbow-color: #00a6dc; }
|
||||
13.75% { --rainbow-color: #00a6e2; }
|
||||
15% { --rainbow-color: #00a4e7; }
|
||||
16.25% { --rainbow-color: #00a3ec; }
|
||||
17.5% { --rainbow-color: #00a2f1; }
|
||||
18.75% { --rainbow-color: #00a0f4; }
|
||||
20% { --rainbow-color: #009ff7; }
|
||||
21.25% { --rainbow-color: #009dfa; }
|
||||
22.5% { --rainbow-color: #009bfc; }
|
||||
23.75% { --rainbow-color: #0098fd; }
|
||||
25% { --rainbow-color: #0096fd; }
|
||||
26.25% { --rainbow-color: #0093fd; }
|
||||
27.5% { --rainbow-color: #2e90fc; }
|
||||
28.75% { --rainbow-color: #4d8dfa; }
|
||||
30% { --rainbow-color: #638af8; }
|
||||
31.25% { --rainbow-color: #7587f5; }
|
||||
32.5% { --rainbow-color: #8583f1; }
|
||||
33.75% { --rainbow-color: #9280ed; }
|
||||
35% { --rainbow-color: #9f7ce9; }
|
||||
36.25% { --rainbow-color: #aa78e3; }
|
||||
37.5% { --rainbow-color: #b574dd; }
|
||||
38.75% { --rainbow-color: #be71d7; }
|
||||
40% { --rainbow-color: #c76dd1; }
|
||||
41.25% { --rainbow-color: #cf69c9; }
|
||||
42.5% { --rainbow-color: #d566c2; }
|
||||
43.75% { --rainbow-color: #dc63ba; }
|
||||
45% { --rainbow-color: #e160b3; }
|
||||
46.25% { --rainbow-color: #e65eab; }
|
||||
47.5% { --rainbow-color: #e95ca2; }
|
||||
48.75% { --rainbow-color: #ed5a9a; }
|
||||
50% { --rainbow-color: #ef5992; }
|
||||
51.25% { --rainbow-color: #f15989; }
|
||||
52.5% { --rainbow-color: #f25981; }
|
||||
53.75% { --rainbow-color: #f25a79; }
|
||||
55% { --rainbow-color: #f25c71; }
|
||||
56.25% { --rainbow-color: #f15e69; }
|
||||
57.5% { --rainbow-color: #ef6061; }
|
||||
58.75% { --rainbow-color: #ed635a; }
|
||||
60% { --rainbow-color: #eb6552; }
|
||||
61.25% { --rainbow-color: #e8694b; }
|
||||
62.5% { --rainbow-color: #e46c44; }
|
||||
63.75% { --rainbow-color: #e06f3d; }
|
||||
65% { --rainbow-color: #db7336; }
|
||||
66.25% { --rainbow-color: #d77630; }
|
||||
67.5% { --rainbow-color: #d17a2a; }
|
||||
68.75% { --rainbow-color: #cc7d24; }
|
||||
70% { --rainbow-color: #c6811e; }
|
||||
71.25% { --rainbow-color: #bf8418; }
|
||||
72.5% { --rainbow-color: #b98713; }
|
||||
73.75% { --rainbow-color: #b28a0f; }
|
||||
75% { --rainbow-color: #ab8d0c; }
|
||||
76.25% { --rainbow-color: #a3900b; }
|
||||
77.5% { --rainbow-color: #9c920d; }
|
||||
78.75% { --rainbow-color: #949510; }
|
||||
80% { --rainbow-color: #8b9715; }
|
||||
81.25% { --rainbow-color: #83991b; }
|
||||
82.5% { --rainbow-color: #7a9b21; }
|
||||
83.75% { --rainbow-color: #719d27; }
|
||||
85% { --rainbow-color: #679e2e; }
|
||||
86.25% { --rainbow-color: #5da035; }
|
||||
87.5% { --rainbow-color: #51a13c; }
|
||||
88.75% { --rainbow-color: #44a244; }
|
||||
90% { --rainbow-color: #34a44b; }
|
||||
91.25% { --rainbow-color: #1ba553; }
|
||||
92.5% { --rainbow-color: #00a65b; }
|
||||
93.75% { --rainbow-color: #00a663; }
|
||||
95% { --rainbow-color: #00a76c; }
|
||||
96.25% { --rainbow-color: #00a874; }
|
||||
97.5% { --rainbow-color: #00a87d; }
|
||||
98.75% { --rainbow-color: #00a985; }
|
||||
100% { --rainbow-color: #00a98e; }
|
||||
}
|
||||
|
||||
:root {
|
||||
--rainbow-color: #00a98e;
|
||||
animation: rainbow 20s linear infinite;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--rainbow-color);
|
||||
user-select: none;
|
||||
background-color: snow;
|
||||
}
|
||||
|
||||
.loading-container *::before,
|
||||
.loading-container *::after {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.loading-container .loading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.loading-container .loading .square {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.loading-container .loading .square::before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
content: "";
|
||||
border: 3px solid var(--rainbow-color);
|
||||
border-radius: 15%;
|
||||
animation: square-to-dot-animation 2s linear infinite;
|
||||
}
|
||||
|
||||
.loading-container .loading .square:nth-child(1)::before {
|
||||
animation-delay: calc(150ms * 1);
|
||||
}
|
||||
|
||||
.loading-container .loading .square:nth-child(2)::before {
|
||||
animation-delay: calc(150ms * 2);
|
||||
}
|
||||
|
||||
.loading-container .loading .square:nth-child(3)::before {
|
||||
animation-delay: calc(150ms * 3);
|
||||
}
|
||||
|
||||
.loading-container .loading .square:nth-child(4)::before {
|
||||
animation-delay: calc(150ms * 4);
|
||||
}
|
||||
|
||||
@keyframes square-to-dot-animation {
|
||||
15%,
|
||||
25% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 5px;
|
||||
border-width: 5px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
40% {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: initial;
|
||||
border-width: 3px;
|
||||
border-radius: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container .name {
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.loading-container .tips {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
opacity: 0.5;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.loading-container .tips::after {
|
||||
position: absolute;
|
||||
padding-left: 5px;
|
||||
content: "…";
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="loading-container">
|
||||
<div class="loading">
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
</div>
|
||||
<div class="name">%VITE_APP_TITLE%</div>
|
||||
<div class="tips">载入中</div>
|
||||
</div>
|
||||
@@ -1,85 +1,119 @@
|
||||
{
|
||||
"version": "3.2.3",
|
||||
"type": "module",
|
||||
"version": "5.0.0-beta.1",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"serve:test": "http-server ./dist-test -o",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build:test": "vue-tsc -b && vite build --mode test",
|
||||
"serve": "http-server ./dist -o",
|
||||
"serve:test": "http-server ./dist-test -o",
|
||||
"svgo": "svgo -f src/assets/icons",
|
||||
"new": "plop",
|
||||
"generate:icons": "esno ./scripts/generate.icons.ts",
|
||||
"lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint:eslint": "eslint --cache --fix --no-ignore --no-error-on-unmatched-pattern --ext .ts,.tsx,.vue src/",
|
||||
"lint:stylelint": "stylelint src/**/*.{css,scss,vue} --cache --fix --allow-empty-input",
|
||||
"postinstall": "simple-git-hooks && esno scripts/prepare.js",
|
||||
"lint:tsc": "vue-tsc -b",
|
||||
"lint:eslint": "eslint . --cache --fix",
|
||||
"lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix",
|
||||
"postinstall": "simple-git-hooks",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"commit": "git cz",
|
||||
"release": "bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
"axios": "^1.5.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.3.14",
|
||||
"eruda": "^3.0.1",
|
||||
"hotkeys-js": "^3.12.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@imengyu/vue3-context-menu": "^1.4.4",
|
||||
"@vee-validate/zod": "^4.15.0",
|
||||
"@vueuse/components": "^12.3.0",
|
||||
"@vueuse/core": "^12.3.0",
|
||||
"@vueuse/integrations": "^12.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"defu": "^6.1.4",
|
||||
"disable-devtool": "^0.3.8",
|
||||
"element-plus": "^2.9.2",
|
||||
"eruda": "^3.4.1",
|
||||
"es-toolkit": "^1.31.0",
|
||||
"hotkeys-js": "^3.13.9",
|
||||
"lucide-vue-next": "^0.469.0",
|
||||
"mitt": "^3.0.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"overlayscrollbars": "^2.10.1",
|
||||
"overlayscrollbars-vue": "^0.5.9",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pinia": "^2.1.6",
|
||||
"qs": "^6.11.2",
|
||||
"path-to-regexp": "^8.2.0",
|
||||
"pinia": "^2.3.0",
|
||||
"qs": "^6.13.1",
|
||||
"radix-vue": "^1.9.12",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"vconsole": "^3.15.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
"vee-validate": "^4.15.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-sonner": "^1.3.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.41.0",
|
||||
"@iconify-json/ep": "^1.1.12",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/nprogress": "^0.2.1",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@types/qs": "^6.9.8",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"bumpp": "^9.2.0",
|
||||
"cz-git": "^1.7.1",
|
||||
"eslint": "^8.51.0",
|
||||
"esno": "^0.17.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"@antfu/eslint-config": "3.12.1",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@iconify/json": "^2.2.291",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@stylistic/stylelint-config": "^2.0.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/qs": "^6.9.17",
|
||||
"@unocss/eslint-plugin": "^0.65.3",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"boxen": "^8.0.1",
|
||||
"bumpp": "^9.9.2",
|
||||
"cz-git": "^1.11.0",
|
||||
"eslint": "^9.17.0",
|
||||
"esno": "^4.8.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"http-server": "^14.1.1",
|
||||
"lint-staged": "^14.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"plop": "^4.0.0",
|
||||
"postcss-html": "^1.5.0",
|
||||
"sass": "^1.69.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-standard-scss": "^11.0.0",
|
||||
"inquirer": "^12.3.0",
|
||||
"lint-staged": "^15.3.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"plop": "^4.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"sass-embedded": "^1.83.1",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-recess-order": "^5.1.1",
|
||||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-scss": "^5.2.1",
|
||||
"stylelint-stylistic": "^0.4.3",
|
||||
"svgo": "^3.0.2",
|
||||
"terser": "^5.21.0",
|
||||
"typescript": "^5.2.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite-plugin-banner": "^0.7.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-mock": "^2.9.8",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-spritesmith": "^0.1.1",
|
||||
"stylelint-scss": "^6.10.0",
|
||||
"svgo": "^3.3.2",
|
||||
"typescript": "^5.7.2",
|
||||
"unocss": "^0.65.3",
|
||||
"unocss-preset-animations": "^1.1.0",
|
||||
"unocss-preset-scrollbar": "^0.3.1",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-turbo-console": "^1.11.1",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-app-loading": "^0.3.0",
|
||||
"vite-plugin-archiver": "^0.1.1",
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite-plugin-env-parse": "^1.0.15",
|
||||
"vite-plugin-fake-server": "^2.1.5",
|
||||
"vite-plugin-pages": "^0.32.4",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-inspector": "^4.0.0",
|
||||
"vite-plugin-vue-meta-layouts": "^0.3.1",
|
||||
"vue-tsc": "^1.8.18"
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
"vite-plugin-vue-meta-layouts": "^0.5.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged",
|
||||
|
||||
@@ -12,6 +12,6 @@ defineOptions({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式
|
||||
<style scoped>
|
||||
/* 样式 */
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const fs = require('node:fs')
|
||||
import fs from 'node:fs'
|
||||
|
||||
function getFolder(path) {
|
||||
const components = []
|
||||
@@ -13,7 +13,7 @@ function getFolder(path) {
|
||||
return components
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
description: '创建组件',
|
||||
prompts: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
|
||||
|
||||
const AllList: any[] = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
AllList.push({
|
||||
id: i + 1,
|
||||
title: faker.color.human(),
|
||||
})
|
||||
}
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/list',
|
||||
method: 'get',
|
||||
response: ({ query }) => {
|
||||
const { title, from, limit } = query
|
||||
const list = AllList.filter((item) => {
|
||||
return title ? item.title.includes(title) : true
|
||||
})
|
||||
const pageList = list.filter((item, index) => {
|
||||
return index >= ~~from && index < (~~from + ~~limit)
|
||||
})
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
list: pageList,
|
||||
total: list.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/detail',
|
||||
method: 'get',
|
||||
response: ({ query }) => {
|
||||
const info = AllList.filter(item => item.id === query.id)
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: info[0],
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/create',
|
||||
method: 'post',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/edit',
|
||||
method: 'post',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/delete',
|
||||
method: 'post',
|
||||
response: () => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
function getFolder(path) {
|
||||
const components = []
|
||||
const files = fs.readdirSync(path)
|
||||
files.forEach((item) => {
|
||||
const stat = fs.lstatSync(`${path}/${item}`)
|
||||
if (stat.isDirectory() === true && item !== 'components') {
|
||||
components.push(`${path}/${item}`)
|
||||
components.push(...getFolder(`${path}/${item}`))
|
||||
}
|
||||
})
|
||||
return components
|
||||
}
|
||||
|
||||
export default {
|
||||
description: '创建标准模块 Mock',
|
||||
prompts: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择模块目录',
|
||||
choices: getFolder('src/views'),
|
||||
},
|
||||
],
|
||||
actions: (data) => {
|
||||
const pathArr = path.relative('src/views', data.path).split('\\')
|
||||
const moduleName = pathArr.pop()
|
||||
const relativePath = pathArr.join('/')
|
||||
const actions = []
|
||||
actions.push({
|
||||
type: 'add',
|
||||
path: pathArr.length === 0 ? 'src/mock/{{moduleName}}.ts' : `src/mock/${pathArr.join('.')}.{{moduleName}}.ts`,
|
||||
templateFile: 'plop-templates/mock/mock.hbs',
|
||||
data: {
|
||||
relativePath,
|
||||
moduleName,
|
||||
},
|
||||
})
|
||||
return actions
|
||||
},
|
||||
}
|
||||
@@ -17,6 +17,6 @@ defineOptions({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 样式
|
||||
<style scoped>
|
||||
/* 样式 */
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
function getFolder(path) {
|
||||
const components = []
|
||||
@@ -14,7 +14,7 @@ function getFolder(path) {
|
||||
return components
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
description: '创建页面',
|
||||
prompts: [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const use{{ properCase name }}Store = defineStore(
|
||||
// 唯一ID
|
||||
'{{ camelCase name }}',
|
||||
{
|
||||
state: () => ({}),
|
||||
getters: {},
|
||||
actions: {},
|
||||
() => {
|
||||
const someThing = ref(0)
|
||||
|
||||
return {
|
||||
someThing,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
description: '创建全局状态',
|
||||
prompts: [
|
||||
{
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
const fs = require('node:fs')
|
||||
import { promises as fs } from 'node:fs'
|
||||
|
||||
module.exports = function (plop) {
|
||||
export default async function (plop) {
|
||||
plop.setWelcomeMessage('请选择需要创建的模式:')
|
||||
fs.readdirSync('./plop-templates').forEach((item) => {
|
||||
fs.lstatSync(`./plop-templates/${item}`).isDirectory() && plop.setGenerator(item, require(`./plop-templates/${item}/prompt`))
|
||||
})
|
||||
const items = await fs.readdir('./plop-templates')
|
||||
for (const item of items) {
|
||||
const stat = await fs.lstat(`./plop-templates/${item}`)
|
||||
if (stat.isDirectory()) {
|
||||
const prompt = await import(`./plop-templates/${item}/prompt.js`)
|
||||
plop.setGenerator(item, prompt.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
'autoprefixer': {},
|
||||
'postcss-nested': {},
|
||||
},
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,20 +1,20 @@
|
||||
#browser-upgrade {
|
||||
position: absolute;
|
||||
z-index: 10001;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 10001;
|
||||
display: none;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #736477;
|
||||
user-select: none;
|
||||
background-color: snow;
|
||||
}
|
||||
|
||||
#browser-upgrade .title {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#browser-upgrade .browsers {
|
||||
@@ -24,8 +24,8 @@
|
||||
#browser-upgrade .browsers .browser {
|
||||
display: inline-block;
|
||||
margin: 0 20px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#browser-upgrade .browsers .browser .browser-icon {
|
||||
@@ -37,10 +37,10 @@
|
||||
}
|
||||
|
||||
#browser-upgrade .browsers .browser .browser-name {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 2px;
|
||||
margin-top: 10px;
|
||||
color: #736477;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,92 +0,0 @@
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fantastic-admin-home {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
color: #736477;
|
||||
background-color: snow;
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 15%;
|
||||
border: 3px solid #8c858f;
|
||||
animation: square-to-dot-animation 2s linear infinite;
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square:nth-child(1)::before {
|
||||
animation-delay: calc(150ms * 1);
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square:nth-child(2)::before {
|
||||
animation-delay: calc(150ms * 2);
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square:nth-child(3)::before {
|
||||
animation-delay: calc(150ms * 3);
|
||||
}
|
||||
|
||||
.fantastic-admin-home .loading .square:nth-child(4)::before {
|
||||
animation-delay: calc(150ms * 4);
|
||||
}
|
||||
|
||||
@keyframes square-to-dot-animation {
|
||||
15%,
|
||||
25% {
|
||||
border-radius: 100%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 5px;
|
||||
border-width: 5px;
|
||||
}
|
||||
|
||||
40% {
|
||||
border-radius: 15%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: initial;
|
||||
border-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.fantastic-admin-home .text {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.fantastic-admin-home .text::after {
|
||||
content: "…";
|
||||
position: absolute;
|
||||
padding-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { exec } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { lookupCollection, lookupCollections } from '@iconify/json'
|
||||
import fs from 'fs-extra'
|
||||
import inquirer from 'inquirer'
|
||||
|
||||
async function generateIcons() {
|
||||
// 拿到全部图标集的原始数据
|
||||
const raw = await lookupCollections()
|
||||
|
||||
let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8')
|
||||
lastChoose = JSON.parse(lastChoose)
|
||||
|
||||
// 取出可使用的图标集数据用于 inquirer 选择,并按名称排序
|
||||
const collections = Object.entries(raw).map(([id, item]) => ({
|
||||
...item,
|
||||
id,
|
||||
})).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
/**
|
||||
* 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用
|
||||
* (1) src/iconify/index.json 记录用户 inquirer 的交互信息
|
||||
* (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名
|
||||
* (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用
|
||||
*/
|
||||
inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
message: '请选择需要生成的图标集',
|
||||
name: 'collections',
|
||||
choices: collections.map(item => ({
|
||||
name: `${item.name} (${item.total} icons)`,
|
||||
value: item.id,
|
||||
})),
|
||||
default: lastChoose.collections,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'isOfflineUse',
|
||||
message: '是否需要离线使用',
|
||||
default: false,
|
||||
},
|
||||
]).then(async (answers) => {
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/index.json'),
|
||||
{
|
||||
collections: answers.collections,
|
||||
isOfflineUse: answers.isOfflineUse,
|
||||
},
|
||||
)
|
||||
|
||||
const outputDir = path.resolve(process.cwd(), 'public/icons')
|
||||
await fs.ensureDir(outputDir)
|
||||
await fs.emptyDir(outputDir)
|
||||
|
||||
const collectionsMeta: object[] = []
|
||||
for (const info of answers.collections) {
|
||||
const setData = await lookupCollection(info)
|
||||
|
||||
collectionsMeta.push({
|
||||
prefix: setData.prefix,
|
||||
info: setData.info,
|
||||
icons: Object.keys(setData.icons),
|
||||
})
|
||||
|
||||
const offlineFilePath = path.join(outputDir, `${info}-raw.json`)
|
||||
|
||||
if (answers.isOfflineUse) {
|
||||
await fs.writeJSON(offlineFilePath, setData)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeJSON(
|
||||
path.resolve(process.cwd(), 'src/iconify/data.json'),
|
||||
collectionsMeta,
|
||||
)
|
||||
|
||||
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
|
||||
})
|
||||
}
|
||||
|
||||
generateIcons()
|
||||
@@ -1,16 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import fs from 'fs-extra'
|
||||
import { icons } from '@iconify-json/ep'
|
||||
|
||||
async function prepare() {
|
||||
const outputDir = path.resolve(process.cwd(), 'public/icons')
|
||||
await fs.ensureDir(outputDir)
|
||||
await fs.emptyDir(outputDir)
|
||||
await fs.writeJSON(
|
||||
path.resolve(outputDir, 'icons.json'),
|
||||
icons,
|
||||
)
|
||||
}
|
||||
|
||||
prepare()
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
// Default options
|
||||
'functions': true,
|
||||
'variableNameTransforms': ['dasherize']
|
||||
}
|
||||
|
||||
{{#block "sprites"}}
|
||||
{{#each sprites}}
|
||||
${{../spritesheet_info.strings.name}}-sprite-{{strings.name}}: ({{px.x}}, {{px.y}}, {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}, {{px.total_width}}, {{px.total_height}}, '{{{escaped_image}}}', '{{name}}');
|
||||
{{/each}}
|
||||
|
||||
${{spritesheet_info.strings.name}}-sprites: (
|
||||
{{#each sprites}}
|
||||
{{strings.name}}: ${{../spritesheet_info.strings.name}}-sprite-{{strings.name}},
|
||||
{{/each}}
|
||||
);
|
||||
{{/block}}
|
||||
|
||||
{{#block "sprite-functions"}}
|
||||
{{#if options.functions}}
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-width($sprite) {
|
||||
width: nth($sprite, 5);
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-height($sprite) {
|
||||
height: nth($sprite, 6);
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-position($sprite) {
|
||||
$sprite-offset-x: nth($sprite, 3);
|
||||
$sprite-offset-y: nth($sprite, 4);
|
||||
background-position: $sprite-offset-x $sprite-offset-y;
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-size($sprite) {
|
||||
background-size: nth($sprite, 7) nth($sprite, 8);
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-image($sprite) {
|
||||
$sprite-image: nth($sprite, 9);
|
||||
background-image: url(#{$sprite-image});
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite($name) {
|
||||
@include {{spritesheet_info.strings.name}}-sprite-image(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
|
||||
@include {{spritesheet_info.strings.name}}-sprite-position(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
|
||||
@include {{spritesheet_info.strings.name}}-sprite-size(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
|
||||
@include {{spritesheet_info.strings.name}}-sprite-width(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
|
||||
@include {{spritesheet_info.strings.name}}-sprite-height(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
{{/if}}
|
||||
{{/block}}
|
||||
|
||||
{{#block "spritesheet-functions"}}
|
||||
{{#if options.functions}}
|
||||
@mixin all-{{spritesheet_info.strings.name}}-sprites() {
|
||||
@each $key, $val in ${{spritesheet_info.strings.name}}-sprites {
|
||||
$sprite-name: nth($val, 10);
|
||||
.{{spritesheet_info.strings.name}}-#{$sprite-name}-sprites {
|
||||
@include {{spritesheet_info.strings.name}}-sprite($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
{{/if}}
|
||||
{{/block}}
|
||||
@@ -1,36 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import eruda from 'eruda'
|
||||
import VConsole from 'vconsole'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import eventBus from './utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { ua } from '@/utils/ua'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Provider from './ui/provider/index.vue'
|
||||
import eventBus from './utils/eventBus'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { auth } = useAuth()
|
||||
|
||||
const buttonConfig = ref({
|
||||
autoInsertSpace: true,
|
||||
})
|
||||
document.body.setAttribute('data-os', ua.getOS().name || '')
|
||||
|
||||
// 侧边栏主导航当前实际宽度
|
||||
const mainSidebarActualWidth = computed(() => {
|
||||
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
|
||||
if (['head', 'single'].includes(settingsStore.settings.menu.menuMode)) {
|
||||
actualWidth = 0
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
})
|
||||
|
||||
// 侧边栏次导航当前实际宽度
|
||||
const subSidebarActualWidth = computed(() => {
|
||||
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
|
||||
if (settingsStore.settings.menu.subMenuCollapse) {
|
||||
actualWidth = 64
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
const isAuth = computed(() => {
|
||||
return route.matched.every((item) => {
|
||||
return auth(item.meta.auth ?? '')
|
||||
})
|
||||
})
|
||||
|
||||
// 设置网页 title
|
||||
watch([
|
||||
() => settingsStore.settings.app.enableDynamicTitle,
|
||||
() => settingsStore.title,
|
||||
@@ -44,34 +32,28 @@ watch([
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
window.onresize = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
}
|
||||
})
|
||||
hotkeys('alt+i', () => {
|
||||
eventBus.emit('global-system-info-toggle')
|
||||
})
|
||||
})
|
||||
|
||||
import.meta.env.VITE_APP_DEBUG_TOOL === 'eruda' && eruda.init()
|
||||
import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn" :size="settingsStore.settings.app.elementSize" :button="buttonConfig">
|
||||
<RouterView
|
||||
v-slot="{ Component, route }"
|
||||
:style="{
|
||||
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
|
||||
'--g-sub-sidebar-actual-width': subSidebarActualWidth,
|
||||
}"
|
||||
>
|
||||
<component :is="Component" v-if="auth(route.meta.auth ?? '')" />
|
||||
<not-allowed v-else />
|
||||
<Provider>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" v-if="isAuth" />
|
||||
<FaNotAllowed v-else />
|
||||
</RouterView>
|
||||
<system-info />
|
||||
</el-config-provider>
|
||||
<FaToast />
|
||||
<FaNotification />
|
||||
<FaSystemInfo />
|
||||
</Provider>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// import qs from 'qs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true') ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY) ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 1000 * 60,
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
(request) => {
|
||||
// 全局拦截请求发送前提交的参数
|
||||
const userStore = useUserStore()
|
||||
/**
|
||||
* 全局拦截请求发送前提交的参数
|
||||
* 以下代码为示例,在请求头里带上 token 信息
|
||||
*/
|
||||
if (userStore.isLogin && request.headers) {
|
||||
request.headers.Token = userStore.token
|
||||
// 设置请求头
|
||||
if (request.headers) {
|
||||
if (userStore.isLogin) {
|
||||
request.headers.Token = userStore.token
|
||||
}
|
||||
}
|
||||
// 是否将 POST 请求参数进行字符串化处理
|
||||
if (request.method === 'post') {
|
||||
@@ -34,23 +33,28 @@ api.interceptors.response.use(
|
||||
(response) => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: {} }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error !== '') {
|
||||
// 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
|
||||
// ElMessage.error(options)
|
||||
toast.warning('Warning', {
|
||||
description: response.data.error,
|
||||
})
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
useUserStore().logout()
|
||||
useUserStore().requestLogout()
|
||||
}
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 401) {
|
||||
useUserStore().requestLogout()
|
||||
throw error
|
||||
}
|
||||
let message = error.message
|
||||
if (message === 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
@@ -61,9 +65,8 @@ api.interceptors.response.use(
|
||||
else if (message.includes('Request failed with status code')) {
|
||||
message = `接口${message.substr(message.length - 3)}异常`
|
||||
}
|
||||
ElMessage({
|
||||
message,
|
||||
type: 'error',
|
||||
toast.error('Error', {
|
||||
description: message,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M768 298.667h170.667V384h-256V128H768v170.667zM341.333 384h-256v-85.333H256V128h85.333v256zM768 725.333V896h-85.333V640h256v85.333H768zM341.333 640v256H256V725.333H85.333V640h256z"/></svg>
|
||||
|
Before Width: | Height: | Size: 299 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M682.667 128h256v256h-85.334V213.333H682.667V128zm-597.334 0h256v85.333H170.667V384H85.333V128zm768 682.667V640h85.334v256h-256v-85.333h170.666zm-682.666 0h170.666V896h-256V640h85.334v170.667z"/></svg>
|
||||
|
Before Width: | Height: | Size: 312 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M704 328a72 72 0 1 0 144 0 72 72 0 1 0-144 0z"/><path d="M999.904 116.608a32 32 0 0 0-21.952-10.912L521.76 73.792a31.552 31.552 0 0 0-27.2 11.904l-92.192 114.848a32 32 0 0 0 .672 40.896l146.144 169.952-147.456 194.656 36.48-173.376a32 32 0 0 0-11.136-31.424L235.616 245.504l79.616-125.696a32 32 0 0 0-29.28-49.024L45.76 87.552a32 32 0 0 0-29.696 34.176l55.808 798.016a32.064 32.064 0 0 0 34.304 29.696l176.512-13.184c17.632-1.312 30.848-16.672 29.504-34.272s-16.576-31.04-34.304-29.536L133.44 883.232l-6.432-92.512 125.312-12.576a32 32 0 0 0 28.672-35.04 32.16 32.16 0 0 0-35.04-28.672L122.56 726.848 82.144 149.184l145.152-10.144-60.96 96.224a32 32 0 0 0 6.848 41.952l198.4 161.344-58.752 279.296a30.912 30.912 0 0 0 .736 14.752 31.68 31.68 0 0 0 1.408 11.04l51.52 154.56a31.968 31.968 0 0 0 27.456 21.76l523.104 47.552a32.064 32.064 0 0 0 34.848-29.632l55.776-798.048a32.064 32.064 0 0 0-7.776-23.232zm-98.912 630.848-412.576-39.648a31.52 31.52 0 0 0-34.912 28.768 32 32 0 0 0 28.8 34.912l414.24 39.808-6.272 89.536-469.728-42.72-39.584-118.72 234.816-310.016a31.936 31.936 0 0 0-1.248-40.192L468.896 219.84l65.088-81.056 407.584 28.48-40.576 580.192z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M512.213 958.99 63.595 510.93l84.025-84.023 300.584 302.222V63.546h127.94V729.13l299.652-302.222 84.01 84.024-447.593 448.058z"/></svg>
|
||||
|
Before Width: | Height: | Size: 244 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M916.21 242.526v215.58H204.8l194.021-194.022-75.453-75.452L0 512l323.368 323.368 75.453-75.452L204.8 565.895H1024V242.526z"/></svg>
|
||||
|
Before Width: | Height: | Size: 240 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m959.804 511.604-84.009 84.024-299.652-302.222V958.99h-127.94V293.406L147.62 595.63l-84.025-84.024L512.213 63.546l447.591 448.058z"/></svg>
|
||||
|
Before Width: | Height: | Size: 248 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M165.415 827.077c-11.815 0-19.692-7.877-19.692-19.692V214.646c0-11.815 7.877-19.692 19.692-19.692h159.508c7.877 0 17.723 3.938 23.63 9.846l228.432 287.508c7.877 9.846 7.877 25.6 0 37.415l-230.4 287.508c-5.908 7.877-15.754 11.815-25.6 11.815l-155.57-1.97zm706.954-334.77L641.97 206.77c-9.846-11.815-27.569-15.754-41.354-3.938l-45.292 37.415c-13.785 9.846-15.754 29.539-3.938 41.354L738.462 512 551.385 744.37c-9.847 11.815-7.877 31.507 3.938 41.353l45.292 37.415c13.785 9.847 29.539 7.877 41.354-3.938l230.4-287.508c7.877-15.754 7.877-29.538 0-39.384z"/></svg>
|
||||
|
Before Width: | Height: | Size: 670 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M487 71.425a75 75 0 0 1 72.575 0l335.7 185.5a75 75 0 0 1 38.75 65.65V690.85a75 75 0 0 1-38.75 65.625l-335.7 185.55a75 75 0 0 1-72.55 0l-335.7-185.5a75 75 0 0 1-38.75-65.675V322.6a75 75 0 0 1 38.75-65.6L487 71.4v.025zM859 322.6 523.275 137.1l-335.7 185.5v368.25l335.7 185.5 335.75-185.5V322.6zm-601.75 37.1A37.5 37.5 0 0 1 308.2 345l215.1 118.875L738.45 345a37.5 37.5 0 0 1 36.25 65.625l-213.9 118.25V764.55a37.5 37.5 0 0 1-75 0v-235.7l-213.9-118.2a37.5 37.5 0 0 1-14.65-51v.05z"/></svg>
|
||||
|
Before Width: | Height: | Size: 597 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M913.6 135.2H380.2c-26.7 0-44.4 17.8-44.4 44.4V224c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zm-755.6 0h-44.4c-22.2 0-44.4 22.2-44.4 44.4V224c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm0 311.1h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zM158 757.4h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 971 B |
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,9 +0,0 @@
|
||||
// 暗黑模式
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
||||
$bg-color: (
|
||||
"page": #0a0a0a,
|
||||
"": #141414,
|
||||
"overlay": #1d1e1f,
|
||||
)
|
||||
);
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
@@ -1,9 +0,0 @@
|
||||
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
// $colors: (
|
||||
// "primary": (
|
||||
// "base": green
|
||||
// )
|
||||
// )
|
||||
// );
|
||||
@use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
@use "./dark.scss";
|
||||
@@ -0,0 +1,115 @@
|
||||
/* 页面布局 CSS 变量 */
|
||||
:root {
|
||||
/* 头部高度 */
|
||||
--g-header-height: 60px;
|
||||
|
||||
/* 侧边栏宽度 */
|
||||
--g-main-sidebar-width: 80px;
|
||||
--g-sub-sidebar-width: 220px;
|
||||
--g-sub-sidebar-collapse-width: 64px;
|
||||
|
||||
/* 侧边栏 Logo 区域高度 */
|
||||
--g-sidebar-logo-height: 50px;
|
||||
|
||||
/* 标签栏高度 */
|
||||
--g-tabbar-height: 50px;
|
||||
|
||||
/* 工具栏高度 */
|
||||
--g-toolbar-height: 50px;
|
||||
}
|
||||
|
||||
/* 明暗模式 CSS 变量 */
|
||||
/* stylelint-disable-next-line no-duplicate-selectors */
|
||||
:root {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
|
||||
&::view-transition-old(root),
|
||||
&::view-transition-new(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 72%);
|
||||
|
||||
&::view-transition-old(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body:not([data-os="macOS"]) {
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--primary) / 30%);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--primary) / 50%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--background));
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* textarea 字体跟随系统 */
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Overrides OverlayScrollbars */
|
||||
[data-overlayscrollbars-contents] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.os-scrollbar {
|
||||
--os-handle-bg: hsl(var(--primary) / 30%);
|
||||
--os-handle-bg-hover: hsl(var(--primary) / 40%);
|
||||
--os-handle-bg-active: hsl(var(--primary) / 50%);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
@use "./themes.scss";
|
||||
@use "./element-plus/index.scss";
|
||||
|
||||
// 全局变量
|
||||
:root {
|
||||
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
|
||||
--g-header-width: 100%;
|
||||
// 头部高度
|
||||
--g-header-height: 70px;
|
||||
// 侧边栏宽度
|
||||
--g-main-sidebar-width: 70px;
|
||||
--g-sub-sidebar-width: 220px;
|
||||
// 侧边栏Logo高度
|
||||
--g-sidebar-logo-height: 50px;
|
||||
// 顶栏高度
|
||||
--g-topbar-height: 50px;
|
||||
}
|
||||
// 全局样式
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(0 0 0 / 40%);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
// 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width]
|
||||
[data-fixed-calc-width] {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
right: 0;
|
||||
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
|
||||
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
|
||||
}
|
||||
|
||||
[data-mode="mobile"] {
|
||||
[data-fixed-calc-width] {
|
||||
width: 100% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
}
|
||||
// textarea 字体跟随系统
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
// 列表页全局样式
|
||||
.el-table.list-table {
|
||||
margin: 20px 0;
|
||||
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-mask {
|
||||
z-index: 900 !important;
|
||||
}
|
||||
@@ -3,48 +3,48 @@
|
||||
|
||||
.bar {
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--g-nprogress-color);
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px hsl(var(--primary)), 0 0 5px hsl(var(--primary));
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0, -4px);
|
||||
box-shadow: 0 0 10px var(--g-nprogress-color), 0 0 5px var(--g-nprogress-color);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 11px;
|
||||
right: 14px;
|
||||
z-index: 2000;
|
||||
display: block;
|
||||
|
||||
.spinner-icon {
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-top-color: hsl(var(--primary));
|
||||
border-left-color: hsl(var(--primary));
|
||||
border-radius: 50%;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
border-top-color: var(--g-nprogress-color);
|
||||
border-left-color: var(--g-nprogress-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
#nprogress .spinner,
|
||||
#nprogress .bar {
|
||||
@@ -7,9 +7,9 @@
|
||||
} @else {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $line;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
}
|
||||
|
||||
@if $type == xy {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
:root {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
--g-app-bg: var(--el-bg-color);
|
||||
// 主区域背景色
|
||||
--g-main-bg: var(--el-bg-color-page);
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: #222b45;
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #fff;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: #334067;
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: #334067;
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: #222b45;
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #eee;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: #334067;
|
||||
// 菜单选中文字颜色
|
||||
--g-main-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-main-sidebar-menu-active-bg: #334067;
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: #fff;
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: #e8e8e8;
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-sub-sidebar-menu-hover-bg: #e1e1e1;
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary);
|
||||
|
||||
/* ----- 顶栏 ----- */
|
||||
--g-toolbar-bg: var(--el-bg-color);
|
||||
|
||||
/* ----- 进度条 ----- */
|
||||
--g-nprogress-color: var(--el-color-primary);
|
||||
|
||||
/* ----- 暗黑模式 ----- */
|
||||
&.dark {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 72%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
--g-app-bg: var(--el-bg-color);
|
||||
// 主区域背景色
|
||||
--g-main-bg: var(--el-bg-color-page);
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: var(--el-bg-color);
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #ccc;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #ccc;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-main-sidebar-menu-active-color: #fff;
|
||||
// 菜单选中背景色
|
||||
--g-main-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: var(--el-bg-color-page);
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-secondary);
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-sub-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: var(--el-text-color-primary);
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 顶栏 ----- */
|
||||
--g-toolbar-bg: var(--el-bg-color);
|
||||
|
||||
/* ----- 进度条 ----- */
|
||||
--g-nprogress-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginForm',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
account?: string
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
onLogin: [account?: string]
|
||||
onRegister: [account?: string]
|
||||
onResetPassword: [account?: string]
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
const loading = ref(false)
|
||||
|
||||
// 登录方式,default 账号密码登录,qrcode 扫码登录
|
||||
const type = ref<'default' | 'qrcode'>('default')
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(z.object({
|
||||
account: z.string().min(1, '请输入用户名'),
|
||||
password: z.string().min(1, '请输入密码'),
|
||||
remember: z.boolean(),
|
||||
})),
|
||||
initialValues: {
|
||||
account: props.account ?? localStorage.getItem('login_account') ?? '',
|
||||
password: '',
|
||||
remember: !!localStorage.getItem('login_account'),
|
||||
},
|
||||
})
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
loading.value = true
|
||||
userStore.login(values).then(() => {
|
||||
if (values.remember) {
|
||||
localStorage.setItem('login_account', values.account)
|
||||
}
|
||||
else {
|
||||
localStorage.removeItem('login_account')
|
||||
}
|
||||
emits('onLogin', values.account)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
|
||||
function testAccount(account: string) {
|
||||
form.setFieldValue('account', account)
|
||||
form.setFieldValue('password', '123456')
|
||||
onSubmit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px w-full flex-col-stretch-center p-12">
|
||||
<div class="mb-6 space-y-2">
|
||||
<h3 class="text-4xl color-[var(--el-text-color-primary)] font-bold">
|
||||
欢迎使用 👋🏻
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground lg:text-base">
|
||||
{{ title }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<FaTabs
|
||||
v-model="type" :list="[
|
||||
{ label: '账号密码登录', value: 'default' },
|
||||
{ label: '扫码登录', value: 'qrcode' },
|
||||
]" class="inline-flex"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="type === 'default'">
|
||||
<form @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="mb-4 flex-center-between">
|
||||
<div class="flex-center-start">
|
||||
<FormField v-slot="{ componentField }" type="checkbox" name="remember">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<FaCheckbox v-bind="componentField">
|
||||
记住我
|
||||
</FaCheckbox>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<FaButton variant="link" class="h-auto p-0" type="button" @click="emits('onResetPassword', form.values.account)">
|
||||
忘记密码了?
|
||||
</FaButton>
|
||||
</div>
|
||||
<FaButton :loading="loading" size="lg" class="w-full" type="submit">
|
||||
登录
|
||||
</FaButton>
|
||||
<div class="mt-4 flex-center gap-2 text-sm">
|
||||
<span class="text-secondary-foreground op-50">还没有帐号?</span>
|
||||
<FaButton variant="link" class="h-auto p-0" type="button" @click="emits('onRegister', form.values.account)">
|
||||
注册新帐号
|
||||
</FaButton>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 text-center -mb-4">
|
||||
<FaDivider>演示账号一键登录</FaDivider>
|
||||
<div class="space-x-2">
|
||||
<FaButton variant="default" size="sm" plain @click="testAccount('admin')">
|
||||
admin
|
||||
</FaButton>
|
||||
<FaButton variant="outline" size="sm" plain @click="testAccount('test')">
|
||||
test
|
||||
</FaButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="type === 'qrcode'">
|
||||
<div class="flex-col-center">
|
||||
<img src="https://s2.loli.net/2024/04/26/GsahtuIZ9XOg5jr.png" class="h-[250px] w-[250px]">
|
||||
<div class="mt-2 text-sm text-secondary-foreground op-50">
|
||||
请使用微信扫码登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
|
||||
defineOptions({
|
||||
name: 'RegisterForm',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
account?: string
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
onLogin: [account?: string]
|
||||
onRegister: [account?: string]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(
|
||||
z.object({
|
||||
account: z.string().min(1, '请输入用户名'),
|
||||
captcha: z.string().min(6, '请输入验证码'),
|
||||
password: z.string().min(1, '请输入密码').min(6, '密码长度为6到18位').max(18, '密码长度为6到18位'),
|
||||
checkPassword: z.string().min(1, '请再次输入密码'),
|
||||
}).refine(data => data.password === data.checkPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
path: ['checkPassword'],
|
||||
}),
|
||||
),
|
||||
initialValues: {
|
||||
account: props.account ?? '',
|
||||
captcha: '',
|
||||
password: '',
|
||||
checkPassword: '',
|
||||
},
|
||||
})
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
loading.value = true
|
||||
emits('onRegister', values.account)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px w-full flex-col-stretch-center p-12">
|
||||
<form @submit="onSubmit">
|
||||
<div class="mb-8 space-y-2">
|
||||
<h3 class="text-4xl color-[var(--el-text-color-primary)] font-bold">
|
||||
探索从这里开始 🚀
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground lg:text-base">
|
||||
演示系统未提供该功能
|
||||
</p>
|
||||
</div>
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField, value, errors }" name="password">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<FaPasswordStrength :password="value" class="mt-2" />
|
||||
</FormDescription>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField, errors }" name="checkPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请再次输入密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FaButton :loading="loading" size="lg" class="mt-4 w-full" type="submit">
|
||||
注册
|
||||
</FaButton>
|
||||
<div class="mt-4 flex-center gap-2 text-sm">
|
||||
<span class="text-secondary-foreground op-50">已经有帐号?</span>
|
||||
<FaButton variant="link" class="h-auto p-0" @click="emits('onLogin', form.values.account)">
|
||||
去登录
|
||||
</FaButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { FormControl, FormField, FormItem, FormMessage } from '@/ui/shadcn/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPasswordForm',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
account?: string
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
onLogin: [account?: string]
|
||||
onResetPassword: [account?: string]
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(z.object({
|
||||
account: z.string().min(1, '请输入用户名'),
|
||||
captcha: z.string().min(6, '请输入验证码'),
|
||||
newPassword: z.string().min(1, '请输入新密码').min(6, '密码长度为6到18位').max(18, '密码长度为6到18位'),
|
||||
})),
|
||||
initialValues: {
|
||||
account: props.account ?? '',
|
||||
captcha: '',
|
||||
newPassword: '',
|
||||
},
|
||||
})
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
loading.value = true
|
||||
emits('onResetPassword', values.account)
|
||||
})
|
||||
|
||||
const countdown = ref(0)
|
||||
const countdownInterval = ref(Number.NaN)
|
||||
function handleSendCaptcha() {
|
||||
countdown.value = 60
|
||||
countdownInterval.value = window.setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value === 0) {
|
||||
clearInterval(countdownInterval.value)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px w-full flex-col-stretch-center p-12">
|
||||
<form @submit="onSubmit">
|
||||
<div class="mb-8 space-y-2">
|
||||
<h3 class="text-4xl color-[var(--el-text-color-primary)] font-bold">
|
||||
忘记密码了? 🔒
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground lg:text-base">
|
||||
演示系统未提供该功能
|
||||
</p>
|
||||
</div>
|
||||
<FormField v-slot="{ componentField, errors }" name="account">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="text" placeholder="请输入用户名" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="flex-start-between gap-2">
|
||||
<FormField v-slot="{ componentField, value, setValue }" name="captcha">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaPinInput :model-value="value" :name="componentField.name" :length="6" class="border-destructive" @update:model-value="val => setValue(val)" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FaButton variant="outline" size="lg" :disabled="countdown > 0" class="flex-1 px-4" @click="handleSendCaptcha">
|
||||
{{ countdown === 0 ? '发送验证码' : `${countdown} 秒后可重新发送` }}
|
||||
</FaButton>
|
||||
</div>
|
||||
<FormField v-slot="{ componentField, errors }" name="newPassword">
|
||||
<FormItem class="relative pb-6 space-y-0">
|
||||
<FormControl>
|
||||
<FaInput type="password" placeholder="请输入新密码" class="w-full" :class="errors.length && 'border-destructive'" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<Transition enter-active-class="transition-opacity" enter-from-class="opacity-0" leave-active-class="transition-opacity" leave-to-class="opacity-0">
|
||||
<FormMessage class="absolute bottom-1 text-xs" />
|
||||
</Transition>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FaButton :loading="loading" size="lg" class="mt-4 w-full" type="submit">
|
||||
确认
|
||||
</FaButton>
|
||||
<div class="mt-4 flex-center gap-2 text-sm color-[var(--el-text-color-secondary)]">
|
||||
<FaButton variant="link" class="h-auto p-0" @click="emits('onLogin', form.values.account)">
|
||||
去登录
|
||||
</FaButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'Auth',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
value: string | string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().auth(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'AuthAll',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
value: string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().authAll(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@ const props = withDefaults(
|
||||
files?: UploadUserFile[]
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
@@ -29,7 +30,7 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'onSuccess': [
|
||||
onSuccess: [
|
||||
res: any,
|
||||
file: UploadUserFile,
|
||||
fileList: UploadUserFile[],
|
||||
@@ -57,10 +58,33 @@ const onExceed: UploadProps['onExceed'] = () => {
|
||||
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
emits('onSuccess', res, file, fileList)
|
||||
}
|
||||
|
||||
const onPreview: UploadProps['onPreview'] = (e) => {
|
||||
const getBlob = (url: string) => new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
getBlob(e.url!).then((blob: any) => {
|
||||
const a = document.createElement('a')
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const event = new MouseEvent('click')
|
||||
a.target = '_blank'
|
||||
a.download = e.name
|
||||
a.href = url
|
||||
a.dispatchEvent(event)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-upload
|
||||
<ElUpload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
@@ -68,12 +92,14 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files as UploadUserFile[]"
|
||||
:on-preview="onPreview"
|
||||
:http-request="httpRequest"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<svg-icon name="ep:upload-filled" class="el-icon--upload" />
|
||||
<FaIcon name="i-ep:upload-filled" class="el-icon--upload" />
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
@@ -81,14 +107,14 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
<template #tip>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
|
||||
<ElAlert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</ElUpload>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
:deep(.el-upload.is-drag) {
|
||||
display: inline-block;
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'FixedActionBar',
|
||||
})
|
||||
|
||||
const isBottom = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
function onScroll() {
|
||||
// 变量scrollTop是滚动条滚动时,滚动条上端距离顶部的距离
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
// 变量windowHeight是可视区的高度
|
||||
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight
|
||||
// 变量scrollHeight是滚动条的总高度(当前可滚动的页面的总高度)
|
||||
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
|
||||
// 滚动条到底部
|
||||
isBottom.value = Math.ceil(scrollTop + windowHeight) >= scrollHeight
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="actionbar" :class="{ shadow: !isBottom }" data-fixed-calc-width>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actionbar {
|
||||
z-index: 4;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 1px 0 var(--g-box-shadow-color);
|
||||
transition: all 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 -10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -25,25 +25,25 @@ const realHeight = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
|
||||
<ElImage :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<svg-icon name="image-load-fail" />
|
||||
<FaIcon name="i-ph:image-broken-duotone" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</ElImage>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.el-image {
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
@@ -52,12 +52,12 @@ const realHeight = computed(() => {
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,17 +12,16 @@ const props = withDefaults(
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
url?: string
|
||||
size?: number
|
||||
width?: number
|
||||
height?: number
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
url: '',
|
||||
size: 2,
|
||||
width: 150,
|
||||
height: 150,
|
||||
@@ -33,14 +32,15 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:url': [
|
||||
url: string,
|
||||
]
|
||||
'onSuccess': [
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
@@ -59,7 +59,7 @@ function previewClose() {
|
||||
}
|
||||
// 移除
|
||||
function remove() {
|
||||
emits('update:url', '')
|
||||
url.value = ''
|
||||
}
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
@@ -89,7 +89,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
<ElUpload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
@@ -98,44 +98,45 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<el-image v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<ElImage v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<template #error>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<svg-icon name="ep:plus" />
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</ElImage>
|
||||
<div v-else class="image">
|
||||
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElImage :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click.stop="preview">
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
<span @click.stop="preview">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<svg-icon name="ep:delete" />
|
||||
<span @click.stop="remove">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
@@ -146,33 +147,35 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
@@ -207,14 +210,14 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
@@ -224,19 +227,21 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
@@ -12,7 +12,6 @@ const props = withDefaults(
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
url?: string[]
|
||||
size?: number
|
||||
max?: number
|
||||
width?: number
|
||||
@@ -20,10 +19,10 @@ const props = withDefaults(
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
url: () => [],
|
||||
size: 2,
|
||||
max: 3,
|
||||
width: 150,
|
||||
@@ -35,14 +34,15 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:url': [
|
||||
value: string[],
|
||||
]
|
||||
'onSuccess': [
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string[]>({
|
||||
default: [],
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
@@ -63,20 +63,16 @@ function previewClose() {
|
||||
}
|
||||
// 移除
|
||||
function remove(index: number) {
|
||||
const url = props.url
|
||||
url.splice(index, 1)
|
||||
emits('update:url', url)
|
||||
url.value.splice(index, 1)
|
||||
}
|
||||
// 移动
|
||||
function move(index: number, type: 'left' | 'right') {
|
||||
const url = props.url
|
||||
if (type === 'left' && index !== 0) {
|
||||
url[index] = url.splice(index - 1, 1, url[index])[0]
|
||||
url.value[index] = url.value.splice(index - 1, 1, url.value[index])[0]
|
||||
}
|
||||
if (type === 'right' && index !== url.length - 1) {
|
||||
url[index] = url.splice(index + 1, 1, url[index])[0]
|
||||
if (type === 'right' && index !== url.value.length - 1) {
|
||||
url.value[index] = url.value.splice(index + 1, 1, url.value[index])[0]
|
||||
}
|
||||
emits('update:url', url)
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
@@ -108,25 +104,25 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in (url as string[])" :key="index" class="images">
|
||||
<el-image v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<ElImage v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click="preview(index)">
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
<span @click="preview(index)">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<svg-icon name="ep:delete" />
|
||||
<span @click="remove(index)">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<svg-icon name="ep:back" />
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<FaIcon name="i-ep:back" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<svg-icon name="ep:right" />
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<FaIcon name="i-ep:right" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
<ElUpload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
@@ -136,27 +132,28 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<svg-icon name="ep:plus" />
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</el-upload>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
@@ -168,35 +165,37 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
.images {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&.disabled {
|
||||
@@ -236,14 +235,14 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
@@ -253,19 +252,21 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'NotAllowed',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const data = ref({
|
||||
inter: Number.NaN,
|
||||
countdown: 5,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
data.value.inter && window.clearInterval(data.value.inter)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
data.value.inter = window.setInterval(() => {
|
||||
data.value.countdown--
|
||||
if (data.value.countdown === 0) {
|
||||
data.value.inter && window.clearInterval(data.value.inter)
|
||||
goBack()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notallowed">
|
||||
<svg-icon name="403" class="icon" />
|
||||
<div class="content">
|
||||
<h1>403</h1>
|
||||
<div class="desc">
|
||||
抱歉,你无权访问该页面
|
||||
</div>
|
||||
<el-button type="primary" @click="goBack">
|
||||
{{ data.countdown }} 秒后,返回首页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notallowed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 700px;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
.icon {
|
||||
font-size: 400px;
|
||||
}
|
||||
|
||||
.content {
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 72px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 20px 0 30px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'PageHeader',
|
||||
})
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
content?: string
|
||||
}>(),
|
||||
{
|
||||
content: '',
|
||||
},
|
||||
)
|
||||
|
||||
const slots = useSlots()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<div class="main">
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot name="content">
|
||||
{{ content }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="slots.default" class="sub">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.main {
|
||||
flex: 1 1 70%;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub {
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'PageMain',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
collaspe?: boolean
|
||||
height?: string
|
||||
}>(),
|
||||
{
|
||||
title: '',
|
||||
collaspe: false,
|
||||
height: '',
|
||||
},
|
||||
)
|
||||
|
||||
const titleSlot = !!useSlots().title
|
||||
|
||||
const collaspeData = ref(props.collaspe)
|
||||
function unCollaspe() {
|
||||
collaspeData.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="page-main" :class="{
|
||||
'is-collaspe': collaspeData,
|
||||
}" :style="{
|
||||
height: collaspeData ? height : '',
|
||||
}"
|
||||
>
|
||||
<div v-if="titleSlot || title" class="title-container">
|
||||
<slot v-if="titleSlot" name="title" />
|
||||
<template v-else>
|
||||
{{ title }}
|
||||
</template>
|
||||
</div>
|
||||
<slot />
|
||||
<div v-if="collaspeData" class="collaspe" title="展开" @click="unCollaspe">
|
||||
<svg-icon name="ep:arrow-down" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-main {
|
||||
position: relative;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.is-collaspe {
|
||||
overflow: hidden;
|
||||
|
||||
.collaspe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 40px 0 10px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
text-shadow: 0 0 1px var(--el-text-color-primary);
|
||||
background: linear-gradient(to bottom, transparent, var(--g-app-bg));
|
||||
transition: background 0.3s, var(--el-transition-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
width: calc(100% + 40px);
|
||||
padding: 14px 20px;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,149 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// 行政区划数据来源于 https://github.com/modood/Administrative-divisions-of-China
|
||||
import pcasRaw from './pcas-code.json'
|
||||
|
||||
defineOptions({
|
||||
name: 'PcasCascader',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string[] | {
|
||||
code: string
|
||||
name: string
|
||||
}[]
|
||||
disabled?: boolean
|
||||
type?: 'pc' | 'pca' | 'pcas'
|
||||
format?: 'code' | 'name' | 'both'
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
type: 'pca',
|
||||
format: 'code',
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:modelValue': [
|
||||
value: string[] | {
|
||||
code: string
|
||||
name: string
|
||||
}[],
|
||||
]
|
||||
}>()
|
||||
|
||||
interface pcasItem {
|
||||
code: string
|
||||
name: string
|
||||
children?: pcasItem[]
|
||||
}
|
||||
|
||||
const pcasData = computed(() => {
|
||||
const data: pcasItem[] = []
|
||||
// 省份
|
||||
pcasRaw.forEach((p) => {
|
||||
const tempP: pcasItem = {
|
||||
code: p.code,
|
||||
name: p.name,
|
||||
}
|
||||
const tempChildrenC: pcasItem[] = []
|
||||
// 城市
|
||||
p.children.forEach((c) => {
|
||||
const tempC: pcasItem = {
|
||||
code: c.code,
|
||||
name: c.name,
|
||||
}
|
||||
if (['pca', 'pcas'].includes(props.type)) {
|
||||
const tempChildrenA: pcasItem[] = []
|
||||
// 区县
|
||||
c.children.forEach((a) => {
|
||||
const tempA: pcasItem = {
|
||||
code: a.code,
|
||||
name: a.name,
|
||||
}
|
||||
if (props.type === 'pcas') {
|
||||
const tempChildrenS: pcasItem[] = []
|
||||
// 街道
|
||||
a.children.forEach((s) => {
|
||||
const tempS: pcasItem = {
|
||||
code: s.code,
|
||||
name: s.name,
|
||||
}
|
||||
tempChildrenS.push(tempS)
|
||||
})
|
||||
tempA.children = tempChildrenS
|
||||
}
|
||||
tempChildrenA.push(tempA)
|
||||
})
|
||||
tempC.children = tempChildrenA
|
||||
}
|
||||
tempChildrenC.push(tempC)
|
||||
})
|
||||
tempP.children = tempChildrenC
|
||||
data.push(tempP)
|
||||
})
|
||||
return data
|
||||
})
|
||||
|
||||
const myValue = computed({
|
||||
// 将入参数据转成 code 码
|
||||
get: () => {
|
||||
return anyToCode(props.modelValue)
|
||||
},
|
||||
// 将 code 码转成出参数据
|
||||
set: (value) => {
|
||||
emits('update:modelValue', value ? codeToAny(value) : [])
|
||||
},
|
||||
})
|
||||
|
||||
function anyToCode(value: any[], dictionarie: any[] = pcasData.value) {
|
||||
const input: string[] = []
|
||||
if (value.length > 0) {
|
||||
const findItem = dictionarie.find((item) => {
|
||||
if (props.format === 'code') {
|
||||
return item.code === value[0]
|
||||
}
|
||||
else if (props.format === 'name') {
|
||||
return item.name === value[0]
|
||||
}
|
||||
else {
|
||||
return item.name === value[0].name && item.code === value[0].code
|
||||
}
|
||||
})
|
||||
input.push(findItem.code)
|
||||
if (findItem.children) {
|
||||
input.push(...anyToCode(value.slice(1 - value.length), findItem.children))
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
function codeToAny(codes: string[], dictionarie: any[] = pcasData.value): any {
|
||||
const output = []
|
||||
const findItem = dictionarie.find(item => item.code === codes[0])
|
||||
if (findItem) {
|
||||
switch (props.format) {
|
||||
case 'code':
|
||||
output.push(findItem.code)
|
||||
break
|
||||
case 'name':
|
||||
output.push(findItem.name)
|
||||
break
|
||||
case 'both':
|
||||
output.push({
|
||||
code: findItem.code,
|
||||
name: findItem.name,
|
||||
})
|
||||
}
|
||||
const newCodes = codes.slice(1 - codes.length)
|
||||
if (newCodes.length > 0 && findItem.children) {
|
||||
output.push(...codeToAny(newCodes, findItem.children))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-cascader v-model="myValue" :options="pcasData as any[]" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
|
||||
</template>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'SearchBar',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fold?: boolean
|
||||
showToggle?: boolean
|
||||
background?: boolean
|
||||
}>(),
|
||||
{
|
||||
fold: true,
|
||||
showToggle: true,
|
||||
background: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:fold': [
|
||||
value: boolean,
|
||||
]
|
||||
'toggle': [
|
||||
value: boolean,
|
||||
]
|
||||
}>()
|
||||
|
||||
const isFold = ref(props.fold)
|
||||
|
||||
watch(() => props.fold, (value) => {
|
||||
isFold.value = value
|
||||
emits('update:fold', value)
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
isFold.value = !isFold.value
|
||||
emits('toggle', isFold.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container" :class="{ 'has-bg': background }">
|
||||
<slot :fold="isFold" />
|
||||
<div v-if="showToggle" class="toggle">
|
||||
<el-button text size="small" @click="toggle">
|
||||
<template #icon>
|
||||
<svg-icon :name="isFold ? 'ep:caret-bottom' : 'ep:caret-top' " />
|
||||
</template>
|
||||
{{ isFold ? '展开' : '收起' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
position: relative;
|
||||
|
||||
&.has-bg {
|
||||
padding: 20px;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
margin-bottom: -10px;
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'SvgIcon',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
flip?: 'horizontal' | 'vertical' | 'both'
|
||||
rotate?: number
|
||||
color?: string
|
||||
size?: string | number
|
||||
}>()
|
||||
|
||||
const style = computed(() => {
|
||||
const transform = []
|
||||
if (props.flip) {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
transform.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
transform.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
transform.push('rotateX(180deg)')
|
||||
transform.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (props.rotate) {
|
||||
transform.push(`rotate(${props.rotate % 360}deg)`)
|
||||
}
|
||||
return {
|
||||
...(props.color && { color: props.color }),
|
||||
...(props.size && { fontSize: typeof props.size === 'number' ? `${props.size}px` : props.size }),
|
||||
...(transform.length && { transform: transform.join(' ') }),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i class="icon" :style="style">
|
||||
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" />
|
||||
<svg v-else aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
</i>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
line-height: 1em;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
fill: currentcolor;
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const { pkg, lastBuildTime } = __SYSTEM_INFO__
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-system-info-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="系统信息" direction="rtl" :size="360">
|
||||
<el-descriptions direction="vertical" :column="1" border>
|
||||
<el-descriptions-item label="最后编译时间" align="center">
|
||||
{{ lastBuildTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions title="生产环境依赖" :column="1" size="small" border>
|
||||
<el-descriptions-item v-for="(val, key) in (pkg.dependencies as object)" :key="key" :label="key">
|
||||
{{ val }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions title="开发环境依赖" :column="1" size="small" border>
|
||||
<el-descriptions-item v-for="(val, key) in (pkg.devDependencies as object)" :key="key" :label="key">
|
||||
{{ val }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'Trend',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string
|
||||
type?: 'up' | 'down'
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
reverse?: boolean
|
||||
}>(),
|
||||
{
|
||||
type: 'up',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
reverse: false,
|
||||
},
|
||||
)
|
||||
|
||||
const isUp = computed(() => {
|
||||
let isUp = props.type === 'up'
|
||||
if (props.reverse) {
|
||||
isUp = !isUp
|
||||
}
|
||||
return isUp
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`trend ${isUp ? 'up' : 'down'}`">
|
||||
<span v-if="prefix" class="prefix">{{ prefix }}</span>
|
||||
<span class="text">{{ value }}</span>
|
||||
<span v-if="suffix" class="suffix">{{ suffix }}</span>
|
||||
<svg-icon name="ep:caret-top" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&.up {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: var(--el-color-danger);
|
||||
|
||||
.icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +0,0 @@
|
||||
import { addCollection } from '@iconify/vue'
|
||||
|
||||
export async function downloadAndInstall() {
|
||||
const data = Object.freeze(await fetch('./icons/icons.json').then(r => r.json()))
|
||||
addCollection(data)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "collections": ["ant-design", "ep", "flagpack", "icon-park", "mdi", "ri", "logos", "twemoji", "vscode-icons"], "isOfflineUse": false }
|
||||
@@ -0,0 +1,9 @@
|
||||
import { addCollection } from '@iconify/vue'
|
||||
import data from './data.json'
|
||||
|
||||
export async function downloadAndInstall(name: string) {
|
||||
const data = Object.freeze(await fetch(`./icons/${name}-raw.json`).then(r => r.json()))
|
||||
addCollection(data)
|
||||
}
|
||||
|
||||
export const icons = data.sort((a, b) => a.info.name.localeCompare(b.info.name))
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import settingsDefault from '@/settings.default'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { diffTwoObj } from '@/utils/object'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'AppSetting',
|
||||
@@ -16,7 +18,7 @@ const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
watch(() => settingsStore.settings.menu.menuMode, (value) => {
|
||||
watch(() => settingsStore.settings.menu.mode, (value) => {
|
||||
if (value === 'single') {
|
||||
menuStore.setActived(0)
|
||||
}
|
||||
@@ -35,473 +37,410 @@ const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
watch(copied, (val) => {
|
||||
if (val) {
|
||||
ElMessage.success('复制成功,请粘贴到 src/settings.ts 文件中!')
|
||||
toast.success('复制成功,请粘贴到 src/settings.ts 文件中!', {
|
||||
position: 'top-center',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function handleCopy() {
|
||||
copy(JSON.stringify(settingsStore.settings, null, 2))
|
||||
copy(JSON.stringify(diffTwoObj(settingsDefault, settingsStore.settings), null, 2))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="应用配置" direction="rtl" :size="360">
|
||||
<el-alert title="应用配置可实时预览效果,但只是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,并将配置粘贴到 src/settings.ts 文件中。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
|
||||
<el-divider>颜色主题</el-divider>
|
||||
<div class="color-scheme">
|
||||
<div class="switch" :class="settingsStore.settings.app.colorScheme" @click="settingsStore.settings.app.colorScheme = settingsStore.settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
|
||||
<svg-icon :name="settingsStore.settings.app.colorScheme === 'light' ? 'ep:sunny' : 'ep:moon'" />
|
||||
</div>
|
||||
<FaDrawer v-model="isShow" title="应用配置" description="在生产环境中应关闭该模块">
|
||||
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
|
||||
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>颜色主题风格</FaDivider>
|
||||
<div class="flex items-center justify-center pb-4">
|
||||
<FaTabs
|
||||
v-model="settingsStore.settings.app.colorScheme"
|
||||
:list="[
|
||||
{ icon: 'i-ri:sun-line', label: '明亮', value: 'light' },
|
||||
{ icon: 'i-ri:moon-line', label: '暗黑', value: 'dark' },
|
||||
{ icon: 'i-codicon:color-mode', label: '系统', value: '' },
|
||||
]"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
<el-divider v-if="settingsStore.mode === 'pc'">
|
||||
导航栏模式
|
||||
</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
|
||||
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.menuMode === 'side' }" @click="settingsStore.settings.menu.menuMode = 'side'">
|
||||
<div class="mode-container" />
|
||||
<svg-icon name="ep:check" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部模式" placement="top" :show-after="500">
|
||||
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.menuMode === 'head' }" @click="settingsStore.settings.menu.menuMode = 'head'">
|
||||
<div class="mode-container" />
|
||||
<svg-icon name="ep:check" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.menuMode === 'single' }" @click="settingsStore.settings.menu.menuMode = 'single'">
|
||||
<div class="mode-container" />
|
||||
<svg-icon name="ep:check" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider>导航栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
主导航切换跳转
|
||||
<el-tooltip content="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
圆角系数
|
||||
</div>
|
||||
<div class="flex-center-start gap-1">
|
||||
<FaButton
|
||||
v-for="(item, index) in [
|
||||
{ label: 0, value: 0 },
|
||||
{ label: 0.25, value: 0.25 },
|
||||
{ label: 0.5, value: 0.5 },
|
||||
{ label: 0.75, value: 0.75 },
|
||||
{ label: 1, value: 1 },
|
||||
]" :key="index" :variant="settingsStore.settings.app.radius === item.value ? 'default' : 'outline'" size="sm" class="w-12" @click="settingsStore.settings.app.radius = (item.value as any)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</FaButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'">
|
||||
<FaDivider>导航栏模式</FaDivider>
|
||||
<div class="menu-mode">
|
||||
<FaTooltip text="侧边栏模式 (含主导航)" :delay="500">
|
||||
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.mode === 'side' }" @click="settingsStore.settings.menu.mode = 'side'">
|
||||
<div class="mode-container" />
|
||||
</div>
|
||||
</FaTooltip>
|
||||
<FaTooltip text="顶部模式" :delay="500">
|
||||
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.mode === 'head' }" @click="settingsStore.settings.menu.mode = 'head'">
|
||||
<div class="mode-container" />
|
||||
</div>
|
||||
</FaTooltip>
|
||||
<FaTooltip text="侧边栏模式 (不含主导航)" :delay="500">
|
||||
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.mode === 'single' }" @click="settingsStore.settings.menu.mode = 'single'">
|
||||
<div class="mode-container" />
|
||||
</div>
|
||||
</FaTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>导航栏</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
主导航点击模式
|
||||
<FaTooltip text="智能选择会判断次导航是否只有且只有一个可访问的菜单进行切换或跳转操作">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<div class="flex-center-start gap-1">
|
||||
<FaButton
|
||||
v-for="(item, index) in [
|
||||
{ label: '切换', value: 'switch' },
|
||||
{ label: '跳转', value: 'jump' },
|
||||
{ label: '智能选择', value: 'smart' },
|
||||
]" :key="index" :variant="settingsStore.settings.menu.mainMenuClickMode === item.value ? 'default' : 'outline'" size="sm" @click="settingsStore.settings.menu.mainMenuClickMode = (item.value as any)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</FaButton>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航保持展开一个
|
||||
<el-tooltip content="开启该功能后,次导航只保持单个菜单的展开" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
<FaTooltip text="开启该功能后,次导航只保持单个菜单的展开">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.menu.subMenuUniqueOpened" />
|
||||
<FaSwitch v-model="settingsStore.settings.menu.subMenuUniqueOpened" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航是否折叠
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.menu.subMenuCollapse" />
|
||||
<FaSwitch v-model="settingsStore.settings.menu.subMenuCollapse" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
显示次导航折叠按钮
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.menu.enableSubMenuCollapseButton" />
|
||||
<FaSwitch v-model="settingsStore.settings.menu.enableSubMenuCollapseButton" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
|
||||
<FaSwitch v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.mode)" />
|
||||
</div>
|
||||
<el-divider>顶栏</el-divider>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>顶栏</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
模式
|
||||
</div>
|
||||
<el-radio-group v-model="settingsStore.settings.topbar.mode" size="small">
|
||||
<el-radio-button label="static">
|
||||
静止
|
||||
</el-radio-button>
|
||||
<el-radio-button label="fixed">
|
||||
固定
|
||||
</el-radio-button>
|
||||
<el-radio-button label="sticky">
|
||||
粘性
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-divider>工具栏</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
<div class="flex-center-start gap-1">
|
||||
<FaButton
|
||||
v-for="(item, index) in [
|
||||
{ label: '静止', value: 'static' },
|
||||
{ label: '固定', value: 'fixed' },
|
||||
{ label: '粘性', value: 'sticky' },
|
||||
]" :key="index" :variant="settingsStore.settings.topbar.mode === item.value ? 'default' : 'outline'" size="sm" @click="settingsStore.settings.topbar.mode = (item.value as any)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</FaButton>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.toolbar.enableFullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.toolbar.enablePageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.toolbar.enableColorScheme" />
|
||||
</div>
|
||||
<el-divider v-if="settingsStore.mode === 'pc'">
|
||||
面包屑导航
|
||||
</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.breadcrumb.enable" />
|
||||
</div>
|
||||
<el-divider>导航搜索</el-divider>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>标签栏</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
<el-tooltip content="对导航进行快捷搜索" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.navSearch.enable" />
|
||||
<FaSwitch v-model="settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否显示图标
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.tabbar.enableIcon" :disabled="!settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.navSearch.enable" />
|
||||
<FaSwitch v-model="settingsStore.settings.tabbar.enableHotkeys" :disabled="!settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
<el-divider>底部版权</el-divider>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>工具栏</FaDivider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
面包屑导航
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.toolbar.breadcrumb" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
导航搜索
|
||||
<FaTooltip text="对导航进行快捷搜索">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.toolbar.navSearch" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.toolbar.fullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<FaTooltip text="使用框架内提供的刷新功能进行页面刷新">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.toolbar.pageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<FaTooltip text="开启后可在明亮/暗黑模式中切换">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.toolbar.colorScheme" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>主页</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.mainPage.enableHotkeys" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>导航搜索</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.toolbar.navSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>底部版权</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.copyright.enable" />
|
||||
<FaSwitch v-model="settingsStore.settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
日期
|
||||
</div>
|
||||
<el-input v-model="settingsStore.settings.copyright.dates" size="small" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
<FaInput v-model="settingsStore.settings.copyright.dates" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
公司
|
||||
</div>
|
||||
<el-input v-model="settingsStore.settings.copyright.company" size="small" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
<FaInput v-model="settingsStore.settings.copyright.company" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
网址
|
||||
</div>
|
||||
<el-input v-model="settingsStore.settings.copyright.website" size="small" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
<FaInput v-model="settingsStore.settings.copyright.website" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
备案
|
||||
</div>
|
||||
<el-input v-model="settingsStore.settings.copyright.beian" size="small" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
<FaInput v-model="settingsStore.settings.copyright.beian" :disabled="!settingsStore.settings.copyright.enable" />
|
||||
</div>
|
||||
<el-divider>主页</el-divider>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>主页</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否开启
|
||||
<el-tooltip content="该功能开启时,登录成功默认进入主页,反之则默认进入导航栏里第一个导航页面" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
是否启用
|
||||
<FaTooltip text="该功能开启时,登录成功默认进入主页,反之则默认进入导航栏里第一个导航页面">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.home.enable" />
|
||||
<FaSwitch v-model="settingsStore.settings.home.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
主页名称
|
||||
<FaTooltip text="开启国际化时,该设置无效">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<el-input v-model="settingsStore.settings.home.title" size="small" />
|
||||
</div>
|
||||
<el-divider>其它</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
组件尺寸
|
||||
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="settingsStore.settings.app.elementSize" size="small">
|
||||
<el-radio-button label="large">
|
||||
较大
|
||||
</el-radio-button>
|
||||
<el-radio-button label="default">
|
||||
默认
|
||||
</el-radio-button>
|
||||
<el-radio-button label="small">
|
||||
稍小
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<FaInput v-model="settingsStore.settings.home.title" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FaDivider>其它</FaDivider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用权限
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.app.enablePermission" />
|
||||
<FaSwitch v-model="settingsStore.settings.app.enablePermission" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
载入进度条
|
||||
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
<FaTooltip text="该功能开启时,跳转路由会看到页面顶部有进度条">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.app.enableProgress" />
|
||||
<FaSwitch v-model="settingsStore.settings.app.enableProgress" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
哀悼模式
|
||||
<FaTooltip text="该功能开启时,整站会变为灰色">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.app.enableMournMode" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
色弱模式
|
||||
</div>
|
||||
<FaSwitch v-model="settingsStore.settings.app.enableColorAmblyopiaMode" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
动态标题
|
||||
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-tooltip>
|
||||
<FaTooltip text="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置">
|
||||
<FaIcon name="i-ri:question-line" />
|
||||
</FaTooltip>
|
||||
</div>
|
||||
<el-switch v-model="settingsStore.settings.app.enableDynamicTitle" />
|
||||
<FaSwitch v-model="settingsStore.settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<el-button type="primary" @click="handleCopy">
|
||||
<template #icon>
|
||||
<svg-icon name="ep:document-copy" />
|
||||
</template>
|
||||
复制配置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<FaButton class="w-full" @click="handleCopy">
|
||||
<FaIcon name="i-ep:document-copy" />
|
||||
复制配置
|
||||
</FaButton>
|
||||
</template>
|
||||
</FaDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
:deep(.el-drawer__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 36px 0 24px;
|
||||
}
|
||||
|
||||
.color-scheme {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
|
||||
$width: 50px;
|
||||
|
||||
.switch {
|
||||
width: $width;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-fill-color-darker);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.dark {
|
||||
.icon {
|
||||
margin-left: calc($width - 24px - 3px);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 3px;
|
||||
padding: 5px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
.menu-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
--uno: flex items-center justify-center gap-4 pb-4;
|
||||
|
||||
.mode {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 55px;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: var(--g-app-bg);
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-lighter);
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px 1px var(--el-border-color-darker);
|
||||
}
|
||||
--uno: relative w-16 h-12 rounded-2 ring-1 ring-border cursor-pointer transition;
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary);
|
||||
--uno: ring-primary ring-2;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after,
|
||||
.mode-container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
--uno: absolute pointer-events-none;
|
||||
}
|
||||
|
||||
.mode-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.2;
|
||||
&::before {
|
||||
--uno: content-empty bg-primary;
|
||||
}
|
||||
|
||||
&::after {
|
||||
--uno: content-empty bg-primary/60;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
--uno: bg-primary/20 border-width-1.5 border-dashed border-primary;
|
||||
|
||||
&::before {
|
||||
--uno: content-empty absolute w-full h-full;
|
||||
}
|
||||
}
|
||||
|
||||
&-side {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
--uno: top-2 bottom-2 left-2 w-2 rounded-tl-1 rounded-bl-1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
--uno: top-2 bottom-2 left-4.5 w-3;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
inset: 5px 5px 5px 40px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
--uno: inset-t-2 inset-r-2 inset-b-2 inset-l-8 rounded-tr-1 rounded-br-1;
|
||||
}
|
||||
}
|
||||
|
||||
&-head {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
height: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
--uno: top-2 left-2 right-2 h-2 rounded-tl-1 rounded-tr-1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
--uno: top-4.5 left-2 bottom-2 w-3 rounded-bl-1;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
inset: 20px 5px 5px 25px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
--uno: inset-t-4.5 inset-r-2 inset-b-2 inset-l-5.5 rounded-br-1;
|
||||
}
|
||||
}
|
||||
|
||||
&-single {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
&::after {
|
||||
--uno: top-2 left-2 bottom-2 w-3 rounded-tl-1 rounded-bl-1;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
inset: 5px 5px 5px 25px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
--uno: inset-t-2 inset-r-2 inset-b-2 inset-l-5.5 rounded-tr-1 rounded-br-1;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active i {
|
||||
display: block;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
--uno: flex items-center justify-between gap-4 px-4 py-2 rounded-lg transition hover-bg-secondary;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--uno: flex items-center flex-shrink-0 gap-2 text-sm;
|
||||
|
||||
i {
|
||||
margin-left: 4px;
|
||||
font-size: 17px;
|
||||
color: var(--el-color-warning);
|
||||
cursor: help;
|
||||
--uno: text-xl text-orange cursor-help;
|
||||
}
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
|
||||
defineOptions({
|
||||
name: 'BackTop',
|
||||
})
|
||||
|
||||
const transitionClass = {
|
||||
enterActiveClass: 'ease-out duration-300',
|
||||
enterFromClass: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
|
||||
enterToClass: 'opacity-100 translate-y-0 lg-scale-100',
|
||||
leaveActiveClass: 'ease-in duration-200',
|
||||
leaveFromClass: 'opacity-100 translate-y-0 lg-scale-100',
|
||||
leaveToClass: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
handleScroll()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
let timeout: (() => void) | undefined
|
||||
|
||||
const show = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
function handleScroll() {
|
||||
scrollTop.value = document.documentElement.scrollTop
|
||||
}
|
||||
watch(scrollTop, (val) => {
|
||||
if (val >= 200) {
|
||||
handleMouseenter()
|
||||
handleMouseleave()
|
||||
}
|
||||
})
|
||||
|
||||
function handleMouseenter() {
|
||||
timeout?.()
|
||||
// 此处setTimeout是为了避免移动端下和click事件同时触发
|
||||
setTimeout(() => {
|
||||
show.value = true
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function handleMouseleave() {
|
||||
;({ stop: timeout } = useTimeoutFn(() => {
|
||||
show.value = false
|
||||
}, 2000))
|
||||
}
|
||||
|
||||
function handleBackToTop() {
|
||||
show.value && document.documentElement.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition v-bind="transitionClass">
|
||||
<FaButton v-if="scrollTop >= 200" variant="outline" size="icon" class="fixed inset-b-4 z-1000 h-12 w-12 rounded-full transition-all -inset-e-9" :class="{ 'inset-e-3!': show }" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" @click="handleBackToTop">
|
||||
<FaIcon name="i-icon-park-outline:to-top-one" :size="24" />
|
||||
</FaButton>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex items-center text-sm">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.breadcrumb-item) {
|
||||
&:first-child {
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.text {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
to?: RouteLocationRaw
|
||||
replace?: boolean
|
||||
separator?: string
|
||||
}>(),
|
||||
{
|
||||
separator: '/',
|
||||
},
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function onClick() {
|
||||
if (props.to) {
|
||||
props.replace ? router.replace(props.to) : router.push(props.to)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="breadcrumb-item flex items-center text-foreground">
|
||||
<span class="separator mx-2">
|
||||
{{ separator }}
|
||||
</span>
|
||||
<span
|
||||
class="text flex items-center opacity-60"
|
||||
:class="{
|
||||
'is-link cursor-pointer transition-opacity hover-opacity-100': !!props.to,
|
||||
}" @click="onClick"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,14 +5,13 @@ defineOptions({
|
||||
name: 'Copyright',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="route.meta.copyright ?? settingsStore.settings.copyright.enable" class="copyright">
|
||||
<footer v-if="settingsStore.settings.copyright.enable" class="copyright">
|
||||
<span>Copyright</span>
|
||||
<span class="icon">©</span>
|
||||
<FaIcon name="i-ri:copyright-line" :size="18" />
|
||||
<span v-if="settingsStore.settings.copyright.dates">{{ settingsStore.settings.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.settings.copyright.company">
|
||||
<a v-if="settingsStore.settings.copyright.website" :href="settingsStore.settings.copyright.website" target="_blank" rel="noopener">{{ settingsStore.settings.copyright.company }}</a>
|
||||
@@ -22,33 +21,17 @@ const settingsStore = useSettingsStore()
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
<style scoped>
|
||||
.copyright {
|
||||
--uno: flex items-center justify-center flex-wrap my-4 px-4 text-sm text-secondary-foreground/50;
|
||||
|
||||
span,
|
||||
a {
|
||||
padding: 0 5px;
|
||||
--uno: px-1;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
--uno: text-center no-underline hover-text-secondary-foreground transition;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Logo from '../Logo/index.vue'
|
||||
import ToolbarRightSide from '../Topbar/Toolbar/rightSide.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutHeader',
|
||||
@@ -12,175 +12,122 @@ const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const { switchTo } = useMenu()
|
||||
|
||||
const navRef = ref()
|
||||
|
||||
// 顶部模式鼠标滚动
|
||||
function handlerMouserScroll(event: WheelEvent) {
|
||||
navRef.value.scrollBy({
|
||||
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.menuMode === 'head'">
|
||||
<Transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.mode === 'head'">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<Logo class="title" />
|
||||
<FaMaskScrollContainer scroll="x" gradient-color="var(--g-header-bg)" class="menu-container h-full flex-1">
|
||||
<!-- 顶部模式 -->
|
||||
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
|
||||
<div class="menu h-full flex of-hidden transition-all">
|
||||
<template v-for="(item, index) in menuStore.allMenus" :key="index">
|
||||
<div v-if="item.children && item.children.length !== 0" class="item-container" :class="{ active: index === menuStore.actived }">
|
||||
<div class="item" @click="switchTo(index)">
|
||||
<svg-icon v-if="item.meta?.icon" :name="item.meta.icon" />
|
||||
<span v-if="item.meta?.title">{{ item.meta.title }}</span>
|
||||
<div
|
||||
class="menu-item relative mx-1 py-2 transition-all" :class="{
|
||||
active: index === menuStore.actived,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" class="group menu-item-container relative h-full w-full flex cursor-pointer items-center justify-between gap-1 rounded-lg px-3 text-[var(--g-header-menu-color)] transition-all hover-(bg-[var(--g-header-menu-hover-bg)] text-[var(--g-header-menu-hover-color)])" :class="{
|
||||
'text-[var(--g-header-menu-active-color)]! bg-[var(--g-header-menu-active-bg)]!': index === menuStore.actived,
|
||||
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
|
||||
>
|
||||
<div class="inline-flex flex-1 items-center justify-center gap-1">
|
||||
<FaIcon v-if="item.meta?.icon" :name="item.meta?.icon" class="menu-item-container-icon transition-transform group-hover-scale-120" />
|
||||
<span class="w-full flex-1 truncate text-sm transition-height transition-opacity transition-width">
|
||||
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Tools />
|
||||
</FaMaskScrollContainer>
|
||||
<ToolbarRightSide />
|
||||
</div>
|
||||
</header>
|
||||
</transition>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
height: var(--g-header-height);
|
||||
padding: 0 20px;
|
||||
margin: 0 auto;
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
box-shadow: -1px 0 0 0 hsl(var(--border)), 1px 0 0 0 hsl(var(--border)), 0 1px 0 0 hsl(var(--border));
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.header-container {
|
||||
width: var(--g-header-width);
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: var(--g-header-width)) {
|
||||
.header-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.title) {
|
||||
position: relative;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
padding: inherit;
|
||||
background-color: inherit;
|
||||
|
||||
.logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 24px;
|
||||
letter-spacing: 1px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 30px;
|
||||
padding: 0 20px;
|
||||
overflow-x: auto;
|
||||
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
|
||||
margin: 0 auto;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
:deep(a.title) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: initial;
|
||||
flex: 0;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
padding: inherit;
|
||||
background-color: inherit;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 5px;
|
||||
width: 80px;
|
||||
.logo {
|
||||
width: initial;
|
||||
max-width: initial;
|
||||
height: min(70%, 50px);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
color: var(--g-header-color);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
.menu {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--g-header-menu-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--g-header-menu-hover-color);
|
||||
background-color: var(--g-header-menu-hover-bg);
|
||||
}
|
||||
:deep(.menu-item) {
|
||||
.menu-item-container {
|
||||
color: var(--g-header-menu-color);
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--g-header-menu-hover-color);
|
||||
background-color: var(--g-header-menu-hover-bg);
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
.menu-item-container-icon {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include text-overflow(1, false);
|
||||
&.active .menu-item-container {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active .item {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.tools) {
|
||||
padding: 0;
|
||||
|
||||
.buttons .item .icon {
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
|
||||
.user-container {
|
||||
font-size: 16px;
|
||||
color: var(--g-header-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 头部动画
|
||||
/* 头部动画 */
|
||||
.header-enter-active,
|
||||
.header-leave-active {
|
||||
transition: transform 0.3s;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
name: 'HotkeysIntro',
|
||||
@@ -18,38 +18,87 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="快捷键介绍" direction="rtl" :size="360">
|
||||
<el-descriptions title="全局" :column="1" border>
|
||||
<el-descriptions-item label="查看系统信息">
|
||||
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + I
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="settingsStore.settings.navSearch.enable && settingsStore.settings.navSearch.enableHotkeys" label="唤起导航搜索">
|
||||
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + S
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.menuMode)" title="主导航" :column="1" border>
|
||||
<el-descriptions-item label="激活下一个主导航">
|
||||
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + `
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
<FaModal v-model="isShow" title="快捷键介绍" :footer="false">
|
||||
<div class="px-4">
|
||||
<div class="grid gap-4 sm-grid-cols-2">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
全局
|
||||
</h2>
|
||||
<ul class="list-none ps-2 pt-2 text-sm">
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>I</FaKbd>
|
||||
</div>
|
||||
查看系统信息
|
||||
</li>
|
||||
<li v-if="settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys" class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>S</FaKbd>
|
||||
</div>
|
||||
唤起导航搜索
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.mode)">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
主导航
|
||||
</h2>
|
||||
<ul class="list-none ps-2 pt-2 text-sm">
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>`</FaKbd>
|
||||
</div>
|
||||
激活下一个主导航
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
标签栏
|
||||
</h2>
|
||||
<ul class="list-none ps-2 pt-2 text-sm">
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>←</FaKbd>
|
||||
</div>
|
||||
切换到上一个标签页
|
||||
</li>
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>→</FaKbd>
|
||||
</div>
|
||||
切换到下一个标签页
|
||||
</li>
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>W</FaKbd>
|
||||
</div>
|
||||
关闭当前标签页
|
||||
</li>
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>1~9</FaKbd>
|
||||
</div>
|
||||
切换到第 n 个标签页
|
||||
</li>
|
||||
<li class="flex-baseline gap-2 py-1">
|
||||
<div class="flex-shrink-0 space-x-1">
|
||||
<FaKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</FaKbd>
|
||||
<FaKbd>0</FaKbd>
|
||||
</div>
|
||||
切换到最后一个标签页
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FaModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: initial;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
transition: var(--el-transition-border);
|
||||
}
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-descriptions__label {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||