mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 00:44:00 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc54fc07e4 | ||
|
|
5a75e566e7 | ||
|
|
47461cdfa5 | ||
|
|
f17fd5ec8c |
@@ -0,0 +1,69 @@
|
||||
// .commitlintrc.js
|
||||
/** @type {import('cz-git').UserConfig} */
|
||||
module.exports = {
|
||||
rules: {
|
||||
// @see: https://commitlint.js.org/#/reference-rules
|
||||
},
|
||||
prompt: {
|
||||
alias: { fd: 'docs: fix typos' },
|
||||
messages: {
|
||||
type: '选择你要提交的类型 :',
|
||||
scope: '选择一个提交范围(可选):',
|
||||
customScope: '请输入自定义的提交范围 :',
|
||||
subject: '填写简短精炼的变更描述 :\n',
|
||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
|
||||
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
|
||||
footerPrefixsSelect: '选择关联issue前缀(可选):',
|
||||
customFooterPrefixs: '输入自定义issue前缀 :',
|
||||
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
|
||||
confirmCommit: '是否提交或修改commit ?'
|
||||
},
|
||||
types: [
|
||||
{ value: 'feat', name: 'feat: ✨ 新增功能 | A new feature', emoji: ':sparkles:' },
|
||||
{ value: 'fix', name: 'fix: 🐛 修复缺陷 | A bug fix', emoji: ':bug:' },
|
||||
{ value: 'docs', name: 'docs: 📝 文档更新 | Documentation only changes', emoji: ':memo:' },
|
||||
{ value: 'style', name: 'style: 💄 代码格式 | Changes that do not affect the meaning of the code', emoji: ':lipstick:' },
|
||||
{ value: 'refactor', name: 'refactor: ♻️ 代码重构 | A code change that neither fixes a bug nor adds a feature', emoji: ':recycle:' },
|
||||
{ value: 'perf', name: 'perf: ⚡️ 性能提升 | A code change that improves performance', emoji: ':zap:' },
|
||||
{ value: 'test', name: 'test: ✅ 测试相关 | Adding missing tests or correcting existing tests', emoji: ':white_check_mark:' },
|
||||
{ value: 'build', name: 'build: 📦️ 构建相关 | Changes that affect the build system or external dependencies', emoji: ':package:' },
|
||||
{ value: 'ci', name: 'ci: 🎡 持续集成 | Changes to our CI configuration files and scripts', emoji: ':ferris_wheel:' },
|
||||
{ value: 'revert', name: 'revert: ⏪️ 回退代码 | Revert to a commit', emoji: ':rewind:' },
|
||||
{ value: 'chore', name: 'chore: 🔨 其他修改 | Other changes that do not modify src or test files', emoji: ':hammer:' },
|
||||
],
|
||||
useEmoji: false,
|
||||
emojiAlign: 'center',
|
||||
themeColorCode: '',
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
customScopesAlign: 'bottom',
|
||||
customScopesAlias: 'custom',
|
||||
emptyScopesAlias: 'empty',
|
||||
upperCaseSubject: false,
|
||||
markBreakingChangeMode: true,
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
breaklineNumber: 100,
|
||||
breaklineChar: '|',
|
||||
skipQuestions: [],
|
||||
issuePrefixs: [
|
||||
// 如果使用 gitee 作为开发管理
|
||||
{ value: 'link', name: 'link: 链接 ISSUES 进行中' },
|
||||
{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' }
|
||||
],
|
||||
customIssuePrefixsAlign: 'top',
|
||||
emptyIssuePrefixsAlias: 'skip',
|
||||
customIssuePrefixsAlias: 'custom',
|
||||
allowCustomIssuePrefixs: true,
|
||||
allowEmptyIssuePrefixs: true,
|
||||
confirmColorize: true,
|
||||
maxHeaderLength: Infinity,
|
||||
maxSubjectLength: Infinity,
|
||||
minSubjectLength: 0,
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: '',
|
||||
defaultIssues: '',
|
||||
defaultScope: '',
|
||||
defaultSubject: ''
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -3,10 +3,7 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
+1
-3
@@ -1,7 +1,7 @@
|
||||
NODE_ENV = production
|
||||
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = Fantastic-admin 基础版
|
||||
VITE_APP_TITLE = 页面标题
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
@@ -11,7 +11,5 @@ VITE_APP_DEBUG_TOOL =
|
||||
VITE_BUILD_MOCK = false
|
||||
# 是否在打包时生成 sourcemap
|
||||
VITE_BUILD_SOURCEMAP = false
|
||||
# 是否在打包时删除 console 代码
|
||||
VITE_BUILD_DROP_CONSOLE = true
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip,brotli
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
NODE_ENV = production
|
||||
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = Fantastic-admin 专业版
|
||||
VITE_APP_TITLE = 页面标题(test)
|
||||
# 接口请求地址,会设置到 axios 的 baseURL 参数上
|
||||
VITE_APP_API_BASEURL = /
|
||||
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
|
||||
@@ -11,7 +11,5 @@ VITE_APP_DEBUG_TOOL =
|
||||
VITE_BUILD_MOCK = true
|
||||
# 是否在打包时生成 sourcemap
|
||||
VITE_BUILD_SOURCEMAP = true
|
||||
# 是否在打包时删除 console 代码
|
||||
VITE_BUILD_DROP_CONSOLE = true
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip,brotli
|
||||
VITE_BUILD_COMPRESS =
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
babel.config.js
|
||||
.eslintrc.js
|
||||
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
globals: {
|
||||
__dirname: true,
|
||||
process: true,
|
||||
require: true,
|
||||
module: true,
|
||||
// element-plus
|
||||
ElMessage: true,
|
||||
ElMessageBox: true,
|
||||
ElNotification: true,
|
||||
ElLoading: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'eslint:recommended',
|
||||
'./.eslintrc-auto-import.json'
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: '2020',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// 代码风格
|
||||
'block-spacing': [2, 'always'],
|
||||
'brace-style': [2, '1tbs', {
|
||||
'allowSingleLine': true
|
||||
}],
|
||||
'comma-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'comma-dangle': [2, 'never'],
|
||||
'comma-style': [2, 'last'],
|
||||
'computed-property-spacing': [2, 'never'],
|
||||
'indent': [2, 4, {
|
||||
'SwitchCase': 1
|
||||
}],
|
||||
'key-spacing': [2, {
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}],
|
||||
'keyword-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'linebreak-style': 0,
|
||||
'multiline-ternary': [2, 'always-multiline'],
|
||||
'no-multiple-empty-lines': [2, {
|
||||
'max': 1
|
||||
}],
|
||||
'no-unneeded-ternary': [2, {
|
||||
'defaultAssignment': false
|
||||
}],
|
||||
'quotes': [2, 'single'],
|
||||
'semi': [2, 'never'],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [2, {
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}],
|
||||
'spaced-comment': [2, 'always', {
|
||||
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
|
||||
}],
|
||||
'switch-colon-spacing': [2, {
|
||||
'after': true,
|
||||
'before': false
|
||||
}],
|
||||
'object-curly-spacing': [2, 'always'],
|
||||
// ES6
|
||||
'arrow-parens': [2, 'as-needed'],
|
||||
'arrow-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
// Vue - https://github.com/vuejs/eslint-plugin-vue
|
||||
'vue/multi-word-component-names': 0,
|
||||
'vue/html-indent': [2, 4],
|
||||
'vue/no-v-html': 0,
|
||||
'vue/max-attributes-per-line': 0,
|
||||
'vue/require-default-prop': 0,
|
||||
'vue/singleline-html-element-content-newline': 0,
|
||||
'vue/require-explicit-emits': [2, {
|
||||
'allowProps': true
|
||||
}],
|
||||
'vue/script-indent': [2, 4, {
|
||||
'switchCase': 1
|
||||
}]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -3,7 +3,5 @@ node_modules
|
||||
dist*
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintrc-auto-import.json
|
||||
public/icons
|
||||
src/assets/sprites/*.*
|
||||
!src/assets/sprites/.gitkeep
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"src/**/*.{js,jsx}": "eslint --fix",
|
||||
"src/**/*.{css,scss}": "stylelint --fix --allow-empty-input",
|
||||
"src/**/*.vue": ["eslint --fix", "stylelint --fix --allow-empty-input"]
|
||||
"src/**/*.{js,jsx,ts,tsx}": "eslint --fix",
|
||||
"src/**/*.{css,scss}": "stylelint --fix --allow-empty-input",
|
||||
"src/**/*.vue": ["eslint --fix", "stylelint --fix --allow-empty-input"]
|
||||
}
|
||||
|
||||
+20
-45
@@ -1,48 +1,23 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-vue/scss"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.scss"],
|
||||
"customSyntax": "postcss-scss"
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"rule-empty-line-before": "never",
|
||||
"at-rule-empty-line-before": "never",
|
||||
"at-rule-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"selector-pseudo-class-no-unknown": null,
|
||||
"property-no-unknown": null,
|
||||
"font-family-no-missing-generic-family-keyword": null,
|
||||
"selector-id-pattern": null,
|
||||
"selector-class-pattern": null,
|
||||
"scss/no-global-function-names": null,
|
||||
"scss/at-import-partial-extension": null,
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreFunctions": [
|
||||
"v-bind",
|
||||
"lighten",
|
||||
"darken"
|
||||
]
|
||||
}
|
||||
],
|
||||
"property-no-vendor-prefix": [
|
||||
true,
|
||||
{
|
||||
"ignoreProperties": [
|
||||
"mask-image"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-line-length": null
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-standard-vue/scss"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.scss"],
|
||||
"customSyntax": "postcss-scss"
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"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,
|
||||
"max-line-length": null
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+7
-7
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"mikestead.dotenv",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"vue.volar"
|
||||
]
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"mikestead.dotenv",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"vue.volar"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+5
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
},
|
||||
"stylelint.validate": ["css", "scss", "vue"]
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
},
|
||||
"stylelint.validate": ["css", "scss", "vue"]
|
||||
}
|
||||
|
||||
+36
-58
@@ -1,62 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title %></title>
|
||||
<%- copyrightScript %>
|
||||
</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"><%= title %>载入中…</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>
|
||||
</a>
|
||||
<a href="https://www.google.cn/chrome/" target="_blank" class="browser">
|
||||
<img class="browser-icon" src="/browser_upgrade/chrome.png" />
|
||||
<div class="browser-name">Google Chrome</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/browser_upgrade/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title %></title>
|
||||
<%- copyrightScript %>
|
||||
</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>
|
||||
<script>
|
||||
if(!!window.ActiveXObject || "ActiveXObject" in window) {
|
||||
document.getElementById('browser-upgrade').style.display = 'block'
|
||||
} else {
|
||||
var Loading = document.querySelector('.loading')
|
||||
Loading.classList.add('animate')
|
||||
Loading.addEventListener('animationend', function() {
|
||||
setTimeout(function() {
|
||||
Loading.classList.remove('animate')
|
||||
}, 600)
|
||||
setTimeout(function() {
|
||||
Loading.classList.add('animate')
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<% if (debugTool == 'eruda') { %>
|
||||
<script src="//unpkg.com/eruda/eruda.js"></script>
|
||||
<script>eruda.init();</script>
|
||||
<% } %>
|
||||
<% if (debugTool == 'vconsole') { %>
|
||||
<script src="//unpkg.com/vconsole/dist/vconsole.min.js"></script>
|
||||
<script>new VConsole();</script>
|
||||
<% } %>
|
||||
</body>
|
||||
<div class="text"><%= title %>载入中…</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>
|
||||
</a>
|
||||
<a href="https://www.google.cn/chrome/" target="_blank" class="browser">
|
||||
<img class="browser-icon" src="/browser_upgrade/chrome.png" />
|
||||
<div class="browser-name">Google Chrome</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- loadingScript %>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<%- devtoolScript %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"allowSyntheticDefaultImports": false,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "dist-test"]
|
||||
}
|
||||
+85
-73
@@ -1,76 +1,88 @@
|
||||
{
|
||||
"name": "fantastic-admin",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:test": "vite build --mode test",
|
||||
"build": "vite build",
|
||||
"serve:test": "http-server ./dist-test -o",
|
||||
"serve": "http-server ./dist -o",
|
||||
"svgo": "svgo -f src/assets/icons",
|
||||
"new": "plop",
|
||||
"lint:eslint": "eslint src/**/*.{js,vue} --fix",
|
||||
"lint:stylelint": "stylelint src/**/*.{css,scss,vue} --fix",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "esno scripts/prepare.js",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-vue": "^5.0.0",
|
||||
"@vueuse/core": "^9.2.0",
|
||||
"@vueuse/integrations": "^9.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"element-plus": "^2.2.16",
|
||||
"hotkeys-js": "^3.10.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"mitt": "^3.0.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pinia": "^2.0.22",
|
||||
"qs": "^6.11.0",
|
||||
"tinymce": "^6.2.0",
|
||||
"vue": "^3.2.39",
|
||||
"vue-router": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ep": "^1.1.7",
|
||||
"@iconify/vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vue/compiler-sfc": "^3.2.39",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"esno": "^0.16.3",
|
||||
"fs-extra": "^10.1.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"plop": "^3.1.1",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.5",
|
||||
"sass": "^1.54.9",
|
||||
"stylelint": "^14.10.0",
|
||||
"stylelint-config-recommended-scss": "^7.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^28.0.0",
|
||||
"stylelint-scss": "^4.3.0",
|
||||
"svgo": "^2.8.0",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"unplugin-vue-components": "^0.22.7",
|
||||
"vite": "^3.1.1",
|
||||
"vite-plugin-banner": "^0.5.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-restart": "^0.2.0",
|
||||
"vite-plugin-spritesmith": "^0.1.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-layouts": "^0.6.0",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vue-eslint-parser": "^9.1.0"
|
||||
"version": "2.0.1",
|
||||
"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",
|
||||
"serve": "http-server ./dist -o",
|
||||
"svgo": "svgo -f src/assets/icons",
|
||||
"new": "plop",
|
||||
"lint:eslint": "eslint src/**/*.{ts,tsx,vue} --fix",
|
||||
"lint:stylelint": "stylelint src/**/*.{css,scss,vue} --fix",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "esno scripts/prepare.js",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"commit": "git cz",
|
||||
"release": "bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-vue": "^5.0.0",
|
||||
"@vueuse/core": "^9.2.0",
|
||||
"@vueuse/integrations": "^9.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"element-plus": "^2.2.17",
|
||||
"hotkeys-js": "^3.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pinia": "^2.0.22",
|
||||
"qs": "^6.11.0",
|
||||
"tinymce": "^6.2.0",
|
||||
"vue": "^3.2.39",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.27.0",
|
||||
"@iconify-json/ep": "^1.1.7",
|
||||
"@iconify/vue": "^4.0.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"bumpp": "^8.2.1",
|
||||
"cz-git": "^1.3.11",
|
||||
"eslint": "^8.24.0",
|
||||
"esno": "^0.16.3",
|
||||
"fs-extra": "^10.1.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"plop": "^3.1.1",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.5",
|
||||
"sass": "^1.55.0",
|
||||
"stylelint": "^14.12.1",
|
||||
"stylelint-config-standard": "^28.0.0",
|
||||
"stylelint-config-standard-scss": "^5.0.0",
|
||||
"stylelint-config-standard-vue": "^1.0.0",
|
||||
"stylelint-scss": "^4.3.0",
|
||||
"svgo": "^2.8.0",
|
||||
"typescript": "^4.8.3",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"unplugin-vue-components": "^0.22.7",
|
||||
"vite": "^3.1.3",
|
||||
"vite-plugin-banner": "^0.5.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-restart": "^0.2.0",
|
||||
"vite-plugin-spritesmith": "^0.1.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-layouts": "^0.6.0",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vue-tsc": "^0.40.13"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-git"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
|
||||
// const router = useRouter()
|
||||
// const route = useRoute()
|
||||
<script lang="ts" setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
|
||||
// const proxy = useCurrentInstance()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 内容区 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 内容区 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,63 +1,64 @@
|
||||
const fs = require('fs')
|
||||
|
||||
function getFolder(path) {
|
||||
let components = []
|
||||
const files = fs.readdirSync(path)
|
||||
files.forEach(function(item) {
|
||||
let stat = fs.lstatSync(path + '/' + item)
|
||||
if (stat.isDirectory() === true && item != 'components') {
|
||||
components.push(path + '/' + item)
|
||||
components.push.apply(components, getFolder(path + '/' + item))
|
||||
}
|
||||
})
|
||||
return components
|
||||
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
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
description: '创建组件',
|
||||
prompts: [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'isGlobal',
|
||||
message: '是否为全局组件',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择组件创建目录',
|
||||
choices: getFolder('src/views'),
|
||||
when: answers => {
|
||||
return !answers.isGlobal
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入组件名称',
|
||||
validate: v => {
|
||||
if (!v || v.trim === '') {
|
||||
return '组件名称不能为空'
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
actions: data => {
|
||||
let path = ''
|
||||
if (data.isGlobal) {
|
||||
path = 'src/components/{{properCase name}}/index.vue'
|
||||
} else {
|
||||
path = `${data.path}/components/{{properCase name}}/index.vue`
|
||||
}
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: path,
|
||||
templateFile: 'plop-templates/component/index.hbs'
|
||||
}
|
||||
]
|
||||
return actions
|
||||
}
|
||||
description: '创建组件',
|
||||
prompts: [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'isGlobal',
|
||||
message: '是否为全局组件',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择组件创建目录',
|
||||
choices: getFolder('src/views'),
|
||||
when: (answers) => {
|
||||
return !answers.isGlobal
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入组件名称',
|
||||
validate: (v) => {
|
||||
if (!v || v.trim === '')
|
||||
return '组件名称不能为空'
|
||||
|
||||
else
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: (data) => {
|
||||
let path = ''
|
||||
if (data.isGlobal)
|
||||
path = 'src/components/{{properCase name}}/index.vue'
|
||||
|
||||
else
|
||||
path = `${data.path}/components/{{properCase name}}/index.vue`
|
||||
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path,
|
||||
templateFile: 'plop-templates/component/index.hbs',
|
||||
},
|
||||
]
|
||||
return actions
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
const AllList = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
AllList.push(Mock.mock({
|
||||
id: '@id',
|
||||
title: '@ctitle(10, 20)'
|
||||
}))
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/list',
|
||||
method: 'get',
|
||||
response: option => {
|
||||
let { title, from, limit } = option.query
|
||||
from = ~~from
|
||||
limit = ~~limit
|
||||
let list = AllList.filter(item => {
|
||||
return title ? item.title.includes(title) : true
|
||||
})
|
||||
let 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: option => {
|
||||
let info = AllList.filter(item => item.id == option.query.id)
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: info[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/create',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/edit',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/delete',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
function getFolder(path) {
|
||||
let components = []
|
||||
const files = fs.readdirSync(path)
|
||||
files.forEach(function(item) {
|
||||
let stat = fs.lstatSync(path + '/' + item)
|
||||
if (stat.isDirectory() === true && item != 'components') {
|
||||
components.push(path + '/' + item)
|
||||
components.push.apply(components, getFolder(path + '/' + item))
|
||||
}
|
||||
})
|
||||
return components
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
description: '创建标准模块 Mock',
|
||||
prompts: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择模块目录',
|
||||
choices: getFolder('src/views')
|
||||
}
|
||||
],
|
||||
actions: data => {
|
||||
let pathArr = path.relative('src/views', data.path).split('\\')
|
||||
let moduleName = pathArr.pop()
|
||||
let relativePath = pathArr.join('/')
|
||||
const actions = []
|
||||
actions.push({
|
||||
type: 'add',
|
||||
path: pathArr.length == 0 ? 'src/mock/{{moduleName}}.js' : `src/mock/${pathArr.join('.')}.{{moduleName}}.js`,
|
||||
templateFile: 'plop-templates/mock/mock.hbs',
|
||||
data: {
|
||||
relativePath,
|
||||
moduleName
|
||||
}
|
||||
})
|
||||
return actions
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
<script setup name="{{ properCase componentName }}">
|
||||
<script lang="ts" setup name="{{ properCase componentName }}">
|
||||
// const proxy = useCurrentInstance()
|
||||
// const router = useRouter()
|
||||
// const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-header title="{{ cname }}" />
|
||||
<page-main>
|
||||
页面内容区域
|
||||
</page-main>
|
||||
</div>
|
||||
<div>
|
||||
页面内容区域
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,60 +2,52 @@ const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
function getFolder(path) {
|
||||
let components = []
|
||||
const files = fs.readdirSync(path)
|
||||
files.forEach(function(item) {
|
||||
let stat = fs.lstatSync(path + '/' + item)
|
||||
if (stat.isDirectory() === true && item != 'components') {
|
||||
components.push(path + '/' + item)
|
||||
components.push.apply(components, getFolder(path + '/' + item))
|
||||
}
|
||||
})
|
||||
return components
|
||||
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
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
description: '创建页面',
|
||||
prompts: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择页面创建目录',
|
||||
choices: getFolder('src/views')
|
||||
description: '创建页面',
|
||||
prompts: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'path',
|
||||
message: '请选择页面创建目录',
|
||||
choices: getFolder('src/views'),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入文件名',
|
||||
validate: (v) => {
|
||||
if (!v || v.trim === '')
|
||||
return '文件名不能为空'
|
||||
|
||||
else
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: (data) => {
|
||||
const relativePath = path.relative('src/views', data.path)
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: `${data.path}/{{dotCase name}}.vue`,
|
||||
templateFile: 'plop-templates/page/index.hbs',
|
||||
data: {
|
||||
componentName: `${relativePath} ${data.name}`,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入文件名',
|
||||
validate: v => {
|
||||
if (!v || v.trim === '') {
|
||||
return '文件名不能为空'
|
||||
} else if (v == 'list' || v == 'detail') {
|
||||
return 'list 和 detail 为保留关键字,无法直接创建,请通过创建标准模块进行生成'
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'cname',
|
||||
message: '请输入页面中文名称',
|
||||
default: '默认页面'
|
||||
}
|
||||
],
|
||||
actions: data => {
|
||||
let relativePath = path.relative('src/views', data.path)
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: `${data.path}/{{dotCase name}}.vue`,
|
||||
templateFile: 'plop-templates/page/index.hbs',
|
||||
data: {
|
||||
componentName: `${relativePath} ${data.name}`
|
||||
}
|
||||
}
|
||||
]
|
||||
return actions
|
||||
}
|
||||
},
|
||||
]
|
||||
return actions
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const use{{ properCase name }}Store = defineStore(
|
||||
// 唯一ID
|
||||
'{{ camelCase name }}',
|
||||
{
|
||||
state: () => ({}),
|
||||
getters: {},
|
||||
actions: {}
|
||||
}
|
||||
// 唯一ID
|
||||
'{{ camelCase name }}',
|
||||
{
|
||||
state: () => ({}),
|
||||
getters: {},
|
||||
actions: {}
|
||||
}
|
||||
)
|
||||
|
||||
export default use{{ properCase name }}Store
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
module.exports = {
|
||||
description: '创建全局状态',
|
||||
prompts: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入模块名称',
|
||||
validate: v => {
|
||||
if (!v || v.trim === '') {
|
||||
return '模块名称不能为空'
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
actions: () => {
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: 'src/store/modules/{{camelCase name}}.js',
|
||||
templateFile: 'plop-templates/store/index.hbs'
|
||||
}
|
||||
]
|
||||
return actions
|
||||
}
|
||||
description: '创建全局状态',
|
||||
prompts: [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: '请输入模块名称',
|
||||
validate: (v) => {
|
||||
if (!v || v.trim === '')
|
||||
return '模块名称不能为空'
|
||||
|
||||
else
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: () => {
|
||||
const actions = [
|
||||
{
|
||||
type: 'add',
|
||||
path: 'src/store/modules/{{camelCase name}}.js',
|
||||
templateFile: 'plop-templates/store/index.hbs',
|
||||
},
|
||||
]
|
||||
return actions
|
||||
},
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,7 +1,6 @@
|
||||
module.exports = function(plop) {
|
||||
plop.setWelcomeMessage('请选择需要创建的模式:')
|
||||
plop.setGenerator('page', require('./plop-templates/page/prompt'))
|
||||
plop.setGenerator('component', require('./plop-templates/component/prompt'))
|
||||
plop.setGenerator('store', require('./plop-templates/store/prompt'))
|
||||
plop.setGenerator('mock', require('./plop-templates/mock/prompt'))
|
||||
module.exports = function (plop) {
|
||||
plop.setWelcomeMessage('请选择需要创建的模式:')
|
||||
plop.setGenerator('page', require('./plop-templates/page/prompt'))
|
||||
plop.setGenerator('component', require('./plop-templates/component/prompt'))
|
||||
plop.setGenerator('store', require('./plop-templates/store/prompt'))
|
||||
}
|
||||
|
||||
Generated
+1331
-506
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+8
-8
@@ -1,15 +1,15 @@
|
||||
import path from 'path'
|
||||
import path from 'node:path'
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
||||
+22
-22
@@ -1,7 +1,7 @@
|
||||
{
|
||||
// Default options
|
||||
'functions': true,
|
||||
'variableNameTransforms': ['dasherize']
|
||||
// Default options
|
||||
'functions': true,
|
||||
'variableNameTransforms': ['dasherize']
|
||||
}
|
||||
|
||||
{{#block "sprites"}}
|
||||
@@ -19,35 +19,35 @@ ${{spritesheet_info.strings.name}}-sprites: (
|
||||
{{#block "sprite-functions"}}
|
||||
{{#if options.functions}}
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-width($sprite) {
|
||||
width: nth($sprite, 5);
|
||||
width: nth($sprite, 5);
|
||||
}
|
||||
|
||||
@mixin {{spritesheet_info.strings.name}}-sprite-height($sprite) {
|
||||
height: nth($sprite, 6);
|
||||
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;
|
||||
$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);
|
||||
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});
|
||||
$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;
|
||||
@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}}
|
||||
@@ -55,12 +55,12 @@ ${{spritesheet_info.strings.name}}-sprites: (
|
||||
{{#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);
|
||||
}
|
||||
@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}}
|
||||
|
||||
+64
-53
@@ -1,89 +1,100 @@
|
||||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
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 useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const buttonConfig = ref({
|
||||
autoInsertSpace: true,
|
||||
})
|
||||
|
||||
// 侧边栏主导航当前实际宽度
|
||||
const mainSidebarActualWidth = computed(() => {
|
||||
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width')
|
||||
actualWidth = parseInt(actualWidth)
|
||||
if (['head', 'single'].includes(settingsStore.menu.menuMode)) {
|
||||
actualWidth = 0
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
let actualWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
|
||||
if (['head', 'single'].includes(settingsStore.menu.menuMode))
|
||||
actualWidth = 0
|
||||
|
||||
return `${actualWidth}px`
|
||||
})
|
||||
|
||||
// 侧边栏次导航当前实际宽度
|
||||
const subSidebarActualWidth = computed(() => {
|
||||
let actualWidth = getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width')
|
||||
actualWidth = parseInt(actualWidth)
|
||||
if (settingsStore.menu.subMenuCollapse) {
|
||||
actualWidth = 64
|
||||
}
|
||||
return `${actualWidth}px`
|
||||
let actualWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
|
||||
if (settingsStore.menu.subMenuCollapse)
|
||||
actualWidth = 64
|
||||
|
||||
return `${actualWidth}px`
|
||||
})
|
||||
|
||||
watch(() => settingsStore.app.colorScheme, () => {
|
||||
if (settingsStore.app.colorScheme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
if (settingsStore.app.colorScheme === 'dark')
|
||||
document.documentElement.classList.add('dark')
|
||||
|
||||
else
|
||||
document.documentElement.classList.remove('dark')
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => settingsStore.mode, () => {
|
||||
if (settingsStore.mode === 'pc') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = settingsStore.subMenuCollapseLastStatus
|
||||
})
|
||||
} else if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
document.body.setAttribute('data-mode', settingsStore.mode)
|
||||
if (settingsStore.mode === 'pc') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = settingsStore.subMenuCollapseLastStatus
|
||||
})
|
||||
}
|
||||
else if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
document.body.setAttribute('data-mode', settingsStore.mode)
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => settingsStore.menu.menuMode, () => {
|
||||
document.body.setAttribute('data-menu-mode', settingsStore.menu.menuMode)
|
||||
document.body.setAttribute('data-menu-mode', settingsStore.menu.menuMode)
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch([
|
||||
() => settingsStore.app.enableDynamicTitle,
|
||||
() => settingsStore.title
|
||||
() => settingsStore.app.enableDynamicTitle,
|
||||
() => settingsStore.title,
|
||||
], () => {
|
||||
if (settingsStore.app.enableDynamicTitle && settingsStore.title) {
|
||||
let title = settingsStore.title
|
||||
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`
|
||||
} else {
|
||||
document.title = import.meta.env.VITE_APP_TITLE
|
||||
}
|
||||
if (settingsStore.app.enableDynamicTitle && settingsStore.title) {
|
||||
const title = settingsStore.title
|
||||
document.title = `${title} - ${import.meta.env.VITE_APP_TITLE}`
|
||||
}
|
||||
else {
|
||||
document.title = import.meta.env.VITE_APP_TITLE
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.onresize = () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
}
|
||||
window.onresize()
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
window.onresize = () => {
|
||||
settingsStore.setMode(document.documentElement.clientWidth)
|
||||
}
|
||||
hotkeys('alt+i', () => {
|
||||
eventBus.emit('global-system-info-toggle')
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn" :size="settingsStore.app.elementSize" :button="{autoInsertSpace: true}">
|
||||
<RouterView
|
||||
:style="{
|
||||
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
|
||||
'--g-sub-sidebar-actual-width': subSidebarActualWidth
|
||||
}"
|
||||
/>
|
||||
</el-config-provider>
|
||||
<el-config-provider :locale="zhCn" :size="settingsStore.app.elementSize" :button="buttonConfig">
|
||||
<RouterView
|
||||
:style="{
|
||||
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
|
||||
'--g-sub-sidebar-actual-width': subSidebarActualWidth,
|
||||
}"
|
||||
/>
|
||||
<system-info />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import router from '@/router/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const toLogin = () => {
|
||||
useUserStore().logout().then(() => {
|
||||
router.push({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: router.currentRoute.value.path !== '/login' ? router.currentRoute.value.fullPath : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true' ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 10000,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
request => {
|
||||
const userStore = useUserStore()
|
||||
/**
|
||||
* 全局拦截请求发送前提交的参数
|
||||
* 以下代码为示例,在请求头里带上 token 信息
|
||||
*/
|
||||
if (userStore.isLogin) {
|
||||
request.headers['Token'] = userStore.token
|
||||
}
|
||||
// 是否将 POST 请求参数进行字符串化处理
|
||||
if (request.method === 'post') {
|
||||
// request.data = qs.stringify(request.data, {
|
||||
// arrayFormat: 'brackets'
|
||||
// })
|
||||
}
|
||||
return request
|
||||
}
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error === '') {
|
||||
// 请求成功并且没有报错
|
||||
return Promise.resolve(response.data)
|
||||
} else {
|
||||
// 这里做错误提示
|
||||
// ElMessage.error(options)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
} else {
|
||||
toLogin()
|
||||
}
|
||||
},
|
||||
error => {
|
||||
let message = error.message
|
||||
if (message == 'Network Error') {
|
||||
message = '后端网络故障'
|
||||
} else if (message.includes('timeout')) {
|
||||
message = '接口请求超时'
|
||||
} else if (message.includes('Request failed with status code')) {
|
||||
message = '接口' + message.substr(message.length - 3) + '异常'
|
||||
}
|
||||
ElMessage({
|
||||
message,
|
||||
type: 'error'
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,83 @@
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
import router from '@/router/index'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const toLogin = () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: router.currentRoute.value.path !== '/login' ? router.currentRoute.value.fullPath : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true' ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
|
||||
timeout: 10000,
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
api.interceptors.request.use(
|
||||
(request) => {
|
||||
const userStore = useUserStore()
|
||||
/**
|
||||
* 全局拦截请求发送前提交的参数
|
||||
* 以下代码为示例,在请求头里带上 token 信息
|
||||
*/
|
||||
if (userStore.isLogin && request.headers)
|
||||
request.headers.Token = userStore.token
|
||||
|
||||
// 是否将 POST 请求参数进行字符串化处理
|
||||
if (request.method === 'post') {
|
||||
// request.data = qs.stringify(request.data, {
|
||||
// arrayFormat: 'brackets',
|
||||
// })
|
||||
}
|
||||
return request
|
||||
},
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
/**
|
||||
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||
* 请求出错时 error 会返回错误信息
|
||||
*/
|
||||
if (response.data.status === 1) {
|
||||
if (response.data.error === '') {
|
||||
// 请求成功并且没有报错
|
||||
return Promise.resolve(response.data)
|
||||
}
|
||||
else {
|
||||
// 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
|
||||
// ElMessage.error(options)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
toLogin()
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
let message = error.message
|
||||
if (message === 'Network Error')
|
||||
message = '后端网络故障'
|
||||
|
||||
else if (message.includes('timeout'))
|
||||
message = '接口请求超时'
|
||||
|
||||
else if (message.includes('Request failed with status code'))
|
||||
message = `接口${message.substr(message.length - 3)}异常`
|
||||
|
||||
ElMessage({
|
||||
message,
|
||||
type: 'error',
|
||||
})
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1623578868505" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5778" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M972.4928 217.4976a73.4208 73.4208 0 0 0-58.5728-64.0512 1415.7312 1415.7312 0 0 1-189.184-42.9568A706.1504 706.1504 0 0 1 555.52 14.4384a77.6704 77.6704 0 0 0-90.112 0 445.8496 445.8496 0 0 1-170.496 96.0512 639.0272 639.0272 0 0 1-183.8592 44.8512A67.8912 67.8912 0 0 0 53.1968 219.392S51.2 379.2384 51.2 520.448C51.2 776.4992 358.1952 1024 511.8464 1024c153.6 0 413.5936-174.592 455.0144-499.712 6.5536-101.888 7.68-204.0832 3.328-306.176H972.8l-0.3072-0.6144z m-167.168 197.9392l-320.512 297.216a43.6736 43.6736 0 0 1-53.248 5.12l-8.5504-7.0144L246.272 529.408a42.752 42.752 0 0 1 2.304-61.184 43.776 43.776 0 0 1 62.1568 2.56l145.7664 150.2208 288.8704-269.6704a44.4416 44.4416 0 0 1 62.5152 2.56c16.5376 17.7664 15.36 45.1072-2.6112 61.44z" p-id="5779"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
||||
|
Before Width: | Height: | Size: 944 B |
@@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
@@ -1 +0,0 @@
|
||||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B |
@@ -1,9 +1,10 @@
|
||||
// 暗黑模式
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
||||
$bg-color: (
|
||||
"page": #0a0a0a,
|
||||
"": #141414,
|
||||
"overlay": #1d1e1f,
|
||||
)
|
||||
$bg-color: (
|
||||
"page": #0a0a0a,
|
||||
"": #141414,
|
||||
"overlay": #1d1e1f,
|
||||
)
|
||||
);
|
||||
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
// $colors: (
|
||||
// "primary": (
|
||||
// "base": green
|
||||
// )
|
||||
// $colors: (
|
||||
// "primary": (
|
||||
// "base": green
|
||||
// )
|
||||
// )
|
||||
// );
|
||||
|
||||
@use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
@use "./dark.scss";
|
||||
|
||||
@@ -3,73 +3,82 @@
|
||||
|
||||
// 全局变量
|
||||
: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;
|
||||
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
|
||||
--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;
|
||||
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;
|
||||
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%);
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: inherit;
|
||||
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));
|
||||
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;
|
||||
}
|
||||
[data-fixed-calc-width] {
|
||||
width: 100% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
}
|
||||
// textarea 字体跟随系统
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
// 列表页全局样式
|
||||
.el-table.list-table {
|
||||
margin: 20px 0;
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
margin: 20px 0;
|
||||
|
||||
.el-button + .el-dropdown {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
.bar {
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--g-nprogress-color);
|
||||
}
|
||||
.peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
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;
|
||||
.spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-radius: 50%;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
border-top-color: var(--g-nprogress-color);
|
||||
border-left-color: var(--g-nprogress-color);
|
||||
}
|
||||
pointer-events: none;
|
||||
|
||||
.bar {
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--g-nprogress-color);
|
||||
}
|
||||
|
||||
.peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
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;
|
||||
|
||||
.spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
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;
|
||||
#nprogress .spinner,
|
||||
#nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
#nprogress .spinner,
|
||||
#nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -1,50 +1,55 @@
|
||||
// 文字超出隐藏,默认为单行超出隐藏,可设置多行
|
||||
@mixin text-overflow($line: 1, $fixed-width: true) {
|
||||
@if $line == 1 and $fixed-width == true {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@else {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $line;
|
||||
overflow: hidden;
|
||||
}
|
||||
@if ($line == 1 and $fixed-width == true) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@else {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: $line;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中
|
||||
@mixin position-center($type: x) {
|
||||
position: absolute;
|
||||
@if $type == x {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
@if $type == y {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
@if $type == xy {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
position: absolute;
|
||||
|
||||
@if ($type == x) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@if ($type == y) {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@if ($type == xy) {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// 文字两端对齐
|
||||
%justify-align {
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
// 清除浮动
|
||||
%clearfix {
|
||||
zoom: 1;
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
zoom: 1;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
:root {
|
||||
--g-box-shadow-color: rgb(0 0 0 / 12%);
|
||||
--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%);
|
||||
|
||||
/* ----- 应用 ----- */
|
||||
// 应用背景色
|
||||
@@ -9,116 +71,54 @@
|
||||
|
||||
/* ----- 顶部 ----- */
|
||||
// 背景色
|
||||
--g-header-bg: #222b45;
|
||||
--g-header-bg: var(--el-bg-color);
|
||||
// 文字颜色
|
||||
--g-header-color: #fff;
|
||||
// 导航文字颜色
|
||||
--g-header-menu-color: #fff;
|
||||
--g-header-menu-color: #ccc;
|
||||
// 导航鼠标悬浮文字颜色
|
||||
--g-header-menu-hover-color: #fff;
|
||||
// 导航鼠标悬浮背景色
|
||||
--g-header-menu-hover-bg: #334067;
|
||||
--g-header-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 导航选中文字颜色
|
||||
--g-header-menu-active-color: #fff;
|
||||
// 导航选中背景色
|
||||
--g-header-menu-active-bg: #334067;
|
||||
--g-header-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 主导航 ----- */
|
||||
// 背景色
|
||||
--g-main-sidebar-bg: #222b45;
|
||||
--g-main-sidebar-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-main-sidebar-menu-color: #eee;
|
||||
--g-main-sidebar-menu-color: #ccc;
|
||||
// 菜单鼠标悬浮文字颜色
|
||||
--g-main-sidebar-menu-hover-color: #fff;
|
||||
// 菜单鼠标悬浮背景色
|
||||
--g-main-sidebar-menu-hover-bg: #334067;
|
||||
--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: #334067;
|
||||
--g-main-sidebar-menu-active-bg: var(--el-color-primary-light-3);
|
||||
|
||||
/* ----- 次导航 ----- */
|
||||
// 背景色
|
||||
--g-sub-sidebar-bg: #fff;
|
||||
--g-sub-sidebar-bg: var(--el-bg-color-page);
|
||||
// 菜单背景色
|
||||
--g-sub-sidebar-menu-bg: #e8e8e8;
|
||||
--g-sub-sidebar-menu-bg: var(--el-bg-color);
|
||||
// 菜单文字颜色
|
||||
--g-sub-sidebar-menu-color: var(--el-text-color-primary);
|
||||
--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: #e1e1e1;
|
||||
--g-sub-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
|
||||
// 菜单选中文字颜色
|
||||
--g-sub-sidebar-menu-active-color: #fff;
|
||||
--g-sub-sidebar-menu-active-color: var(--el-text-color-primary);
|
||||
// 菜单选中背景色
|
||||
--g-sub-sidebar-menu-active-bg: var(--el-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);
|
||||
|
||||
/* ----- 暗黑模式 ----- */
|
||||
&.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('./utils/composables/useAuth')['default']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentInstance: typeof import('./utils/composables/useCurrentInstance')['default']
|
||||
const useMainPage: typeof import('./utils/composables/useMainPage')['default']
|
||||
const useMenu: typeof import('./utils/composables/useMenu')['default']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
Auth: typeof import('./components/Auth/index.vue')['default']
|
||||
AuthAll: typeof import('./components/AuthAll/index.vue')['default']
|
||||
BatchActionBar: typeof import('./components/BatchActionBar/index.vue')['default']
|
||||
Copyright: typeof import('./components/Copyright/index.vue')['default']
|
||||
Editor: typeof import('./components/Editor/index.vue')['default']
|
||||
FileUpload: typeof import('./components/FileUpload/index.vue')['default']
|
||||
FixedActionBar: typeof import('./components/FixedActionBar/index.vue')['default']
|
||||
ImagePreview: typeof import('./components/ImagePreview/index.vue')['default']
|
||||
ImagesUpload: typeof import('./components/ImagesUpload/index.vue')['default']
|
||||
ImageUpload: typeof import('./components/ImageUpload/index.vue')['default']
|
||||
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
|
||||
PageMain: typeof import('./components/PageMain/index.vue')['default']
|
||||
PcasCascader: typeof import('./components/PcasCascader/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchBar: typeof import('./components/SearchBar/index.vue')['default']
|
||||
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']
|
||||
SystemInfo: typeof import('./components/SystemInfo/index.vue')['default']
|
||||
Trend: typeof import('./components/Trend/index.vue')['default']
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
<script setup name="Auth">
|
||||
import { useAuth } from '@/utils/composables'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
<script lang="ts" setup name="Auth">
|
||||
const props = defineProps<{
|
||||
value: string | string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().auth(props.value)
|
||||
return useAuth().auth(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<script setup name="AuthAll">
|
||||
import { useAuth } from '@/utils/composables'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
<script lang="ts" setup name="AuthAll">
|
||||
const props = defineProps<{
|
||||
value: string[]
|
||||
}>()
|
||||
|
||||
function check() {
|
||||
return useAuth().authAll(props.value)
|
||||
return useAuth().authAll(props.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
<div>
|
||||
<slot v-if="check()" />
|
||||
<slot v-else name="no-auth" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,73 +1,82 @@
|
||||
<script setup name="BatchActionBar">
|
||||
<script lang="ts" setup name="BatchActionBar">
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectionData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectionData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['check-all', 'check-null'])
|
||||
const emit = defineEmits(['checkAll', 'checkNull'])
|
||||
|
||||
const checkAll = computed({
|
||||
get: function() {
|
||||
let flag = false
|
||||
if (props.data.length != 0 && props.data.length == props.selectionData.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
},
|
||||
set: function(newVal) {
|
||||
newVal ? emit('check-all') : emit('check-null')
|
||||
}
|
||||
get() {
|
||||
let flag = false
|
||||
if (props.data.length !== 0 && props.data.length === props.selectionData.length)
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
},
|
||||
set(newVal) {
|
||||
newVal ? emit('checkAll') : emit('checkNull')
|
||||
},
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
let flag = false
|
||||
if (props.selectionData.length > 0 && props.selectionData.length < props.data.length) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
let flag = false
|
||||
if (props.selectionData.length > 0 && props.selectionData.length < props.data.length)
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="batch-action-bar">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" :disabled="!data.length">当页全选</el-checkbox>
|
||||
<div v-if="selectionData.length" class="tips">已选 {{ selectionData.length }} 项</div>
|
||||
<el-form :disabled="!selectionData.length">
|
||||
<slot />
|
||||
</el-form>
|
||||
<div class="batch-action-bar">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" :disabled="!data.length">
|
||||
当页全选
|
||||
</el-checkbox>
|
||||
<div v-if="selectionData.length" class="tips">
|
||||
已选 {{ selectionData.length }} 项
|
||||
</div>
|
||||
<el-form :disabled="!selectionData.length">
|
||||
<slot />
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.batch-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-right: 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
> .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.el-checkbox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tips {
|
||||
margin-right: 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.el-form) {
|
||||
> .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
> .el-button-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
> .el-button-group {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,50 @@
|
||||
<script setup name="Copyright">
|
||||
<script lang="ts" setup name="Copyright">
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="copyright">
|
||||
<span>Copyright</span>
|
||||
<span class="icon">©</span>
|
||||
<span v-if="settingsStore.copyright.dates">{{ settingsStore.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.copyright.company">
|
||||
<a v-if="settingsStore.copyright.website" :href="settingsStore.copyright.website" target="_blank" rel="noopener">{{ settingsStore.copyright.company }}</a>
|
||||
<span v-else>{{ settingsStore.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ settingsStore.copyright.beian }}</a>
|
||||
</footer>
|
||||
<footer v-if="route.meta.copyright ?? settingsStore.copyright.enable" class="copyright">
|
||||
<span>Copyright</span>
|
||||
<span class="icon">©</span>
|
||||
<span v-if="settingsStore.copyright.dates">{{ settingsStore.copyright.dates }}</span>
|
||||
<template v-if="settingsStore.copyright.company">
|
||||
<a v-if="settingsStore.copyright.website" :href="settingsStore.copyright.website" target="_blank" rel="noopener">{{ settingsStore.copyright.company }}</a>
|
||||
<span v-else>{{ settingsStore.copyright.company }}</span>
|
||||
</template>
|
||||
<a v-if="settingsStore.copyright.beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ settingsStore.copyright.beian }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
|
||||
span,
|
||||
a {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
span,
|
||||
a {
|
||||
padding: 0 5px;
|
||||
}
|
||||
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;
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup name="Editor">
|
||||
<script lang="ts" setup name="Editor">
|
||||
import tinymce from 'tinymce/tinymce'
|
||||
import TinymceEditor from '@tinymce/tinymce-vue'
|
||||
import 'tinymce/themes/silver/theme'
|
||||
@@ -20,78 +20,78 @@ import 'tinymce/plugins/searchreplace'
|
||||
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
setting: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const defaultSetting = ref({
|
||||
language_url: 'tinymce/langs/zh-Hans.js',
|
||||
language: 'zh-Hans',
|
||||
skin_url: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/ui/oxide' : 'tinymce/skins/ui/oxide-dark',
|
||||
content_css: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/content/default/content.min.css' : 'tinymce/skins/content/dark/content.min.css',
|
||||
min_height: 250,
|
||||
max_height: 600,
|
||||
selector: 'textarea',
|
||||
plugins: 'autolink autoresize fullscreen image insertdatetime link lists media preview table wordcount code searchreplace',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor removeformat | link image media table insertdatetime searchreplace | preview code fullscreen',
|
||||
branding: false,
|
||||
menubar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
insertdatetime_formats: [
|
||||
'%Y年%m月%d日',
|
||||
'%H点%M分%S秒',
|
||||
'%Y-%m-%d',
|
||||
'%H:%M:%S'
|
||||
],
|
||||
// https://www.tiny.cloud/docs/tinymce/6/file-image-upload/#images_upload_handler
|
||||
images_upload_handler: blobInfo => new Promise(resolve => {
|
||||
const img = `data:image/jpeg;base64,${blobInfo.base64()}`
|
||||
resolve(img)
|
||||
})
|
||||
language_url: 'tinymce/langs/zh-Hans.js',
|
||||
language: 'zh-Hans',
|
||||
skin_url: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/ui/oxide' : 'tinymce/skins/ui/oxide-dark',
|
||||
content_css: settingsStore.app.colorScheme === 'light' ? 'tinymce/skins/content/default/content.min.css' : 'tinymce/skins/content/dark/content.min.css',
|
||||
min_height: 250,
|
||||
max_height: 600,
|
||||
selector: 'textarea',
|
||||
plugins: 'autolink autoresize fullscreen image insertdatetime link lists media preview table wordcount code searchreplace',
|
||||
toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor removeformat | link image media table insertdatetime searchreplace | preview code fullscreen',
|
||||
branding: false,
|
||||
menubar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
insertdatetime_formats: [
|
||||
'%Y年%m月%d日',
|
||||
'%H点%M分%S秒',
|
||||
'%Y-%m-%d',
|
||||
'%H:%M:%S',
|
||||
],
|
||||
// https://www.tiny.cloud/docs/tinymce/6/file-image-upload/#images_upload_handler
|
||||
images_upload_handler: (blobInfo: any, progress: any) => new Promise((resolve, reject) => {
|
||||
const img = `data:image/jpeg;base64,${blobInfo.base64()}`
|
||||
resolve(img)
|
||||
}),
|
||||
})
|
||||
|
||||
const myValue = ref(props.modelValue)
|
||||
|
||||
const completeSetting = computed(() => {
|
||||
return Object.assign(defaultSetting.value, props.setting)
|
||||
return Object.assign(defaultSetting.value, props.setting)
|
||||
})
|
||||
|
||||
watch(() => myValue.value, newValue => {
|
||||
emit('update:modelValue', newValue)
|
||||
watch(() => myValue.value, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, newValue => {
|
||||
myValue.value = newValue
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
myValue.value = newValue
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
tinymce.init({})
|
||||
tinymce.init({})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor">
|
||||
<TinymceEditor v-model="myValue" :init="completeSetting" :disabled="disabled" />
|
||||
</div>
|
||||
<div class="editor">
|
||||
<TinymceEditor v-model="myValue" :init="completeSetting" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.tox-tinymce) {
|
||||
border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,107 +1,117 @@
|
||||
<script setup name="FileUpload">
|
||||
<script lang="ts" setup name="FileUpload">
|
||||
import { UploadUserFile } from 'element-plus'
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['zip', 'rar']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['zip', 'rar'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
const emit = defineEmits(['onSuccess'])
|
||||
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传文件只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onExceed() {
|
||||
ElMessage.warning('文件上传超过限制')
|
||||
|
||||
const onExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('文件上传超过限制')
|
||||
}
|
||||
function onSuccess(res, file, fileList) {
|
||||
emit('on-success', res, file, fileList)
|
||||
|
||||
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
emit('onSuccess', res, file, fileList)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-upload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<el-icon class="el-icon--upload">
|
||||
<svg-icon name="ep:upload-filled" />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<el-upload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:file-list="files as UploadUserFile[]"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<el-icon class="el-icon--upload">
|
||||
<svg-icon name="ep:upload-filled" />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-upload.is-drag) {
|
||||
display: inline-block;
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
display: inline-block;
|
||||
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
<script setup name="FixedActionBar">
|
||||
const data = ref({
|
||||
isBottom: true
|
||||
})
|
||||
<script lang="ts" setup name="FixedActionBar">
|
||||
const isBottom = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
|
||||
function onScroll() {
|
||||
// 变量scrollTop是滚动条滚动时,滚动条上端距离顶部的距离
|
||||
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||
// 变量windowHeight是可视区的高度
|
||||
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight
|
||||
// 变量scrollHeight是滚动条的总高度(当前可滚动的页面的总高度)
|
||||
var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
|
||||
// 滚动条到底部
|
||||
if (Math.ceil(scrollTop + windowHeight) >= scrollHeight) {
|
||||
data.value.isBottom = true
|
||||
} else {
|
||||
data.value.isBottom = false
|
||||
}
|
||||
// 变量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': !data.isBottom}" data-fixed-calc-width>
|
||||
<slot />
|
||||
</div>
|
||||
<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);
|
||||
}
|
||||
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;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
<script setup name="ImagePreview">
|
||||
<script lang="ts" setup name="ImagePreview">
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
}
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const realWidth = computed(() => {
|
||||
return typeof props.width == 'string' ? props.width : `${props.width}px`
|
||||
return typeof props.width === 'string' ? props.width : `${props.width}px`
|
||||
})
|
||||
|
||||
const realHeight = computed(() => {
|
||||
return typeof props.height == 'string' ? props.height : `${props.height}px`
|
||||
return typeof props.height === 'string' ? props.height : `${props.height}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<svg-icon name="image-load-fail" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<svg-icon name="image-load-fail" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-image {
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color);
|
||||
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;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--el-fill-color);
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,242 +1,263 @@
|
||||
<script setup name="ImageUpload">
|
||||
<script lang="ts" setup name="ImageUpload">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:url', 'on-success'])
|
||||
const emit = defineEmits(['update:url', 'onSuccess'])
|
||||
|
||||
const uploadData = ref({
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview() {
|
||||
uploadData.value.imageViewerVisible = true
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove() {
|
||||
emit('update:url', '')
|
||||
emit('update:url', '')
|
||||
}
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传图片只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
|
||||
if (isTypeOk && isSizeOk)
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onProgress(file) {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
function onSuccess(res) {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('on-success', res)
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<el-image 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;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="image">
|
||||
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click.stop="preview">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-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" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<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" />
|
||||
</div>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<el-image 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;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="image">
|
||||
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click.stop="preview">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click.stop="remove">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" @close="previewClose" />
|
||||
</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" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include position-center(xy);
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,275 +1,298 @@
|
||||
<script setup name="ImagesUpload">
|
||||
<script lang="ts" setup name="ImagesUpload">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
url: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp']
|
||||
}
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
},
|
||||
url: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 150,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
notip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ext: {
|
||||
type: Array,
|
||||
default: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:url', 'on-success'])
|
||||
const emit = defineEmits(['update:url', 'onSuccess'])
|
||||
|
||||
const uploadData = ref({
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0
|
||||
}
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview(index) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
function preview(index: number) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove(index) {
|
||||
let url = props.url
|
||||
url.splice(index, 1)
|
||||
emit('update:url', url)
|
||||
function remove(index: number) {
|
||||
const url = props.url
|
||||
url.splice(index, 1)
|
||||
emit('update:url', url)
|
||||
}
|
||||
// 移动
|
||||
function move(index, type) {
|
||||
let url = props.url
|
||||
if (type == 'left' && index != 0) {
|
||||
url[index] = url.splice(index - 1, 1, url[index])[0]
|
||||
}
|
||||
if (type == 'right' && index != url.length - 1) {
|
||||
url[index] = url.splice(index + 1, 1, url[index])[0]
|
||||
}
|
||||
emit('update:url', url)
|
||||
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]
|
||||
|
||||
if (type === 'right' && index !== url.length - 1)
|
||||
url[index] = url.splice(index + 1, 1, url[index])[0]
|
||||
|
||||
emit('update:url', url)
|
||||
}
|
||||
function beforeUpload(file) {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName[fileName.length - 1]
|
||||
const isTypeOk = props.ext.indexOf(fileExt) >= 0
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
ElMessage.error(`上传图片只支持 ${ props.ext.join(' / ') } 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1)
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk)
|
||||
ElMessage.error(`上传图片只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
|
||||
if (!isSizeOk)
|
||||
ElMessage.error(`上传图片大小不能超过 ${props.size}MB!`)
|
||||
|
||||
if (isTypeOk && isSizeOk)
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
function onProgress(file) {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
function onSuccess(res) {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('on-success', res)
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emit('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in url" :key="index" class="images">
|
||||
<el-image 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)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{'disabled': index == 0}" @click="move(index, 'left')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:back" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{'disabled': index == url.length - 1}" @click="move(index, 'right')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span title="预览" @click="preview(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:zoom-in" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span title="移除" @click="remove(index)">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:delete" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="左移" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:back" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-show="url.length > 1" title="右移" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-upload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-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" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url" :initial-index="uploadData.dialogImageIndex" @close="previewClose" />
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:plus" />
|
||||
</el-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" />
|
||||
</div>
|
||||
</el-upload>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.images {
|
||||
position: relative;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
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);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
transition: opacity 0.3s;
|
||||
.actions {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include position-center(xy);
|
||||
span {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--el-color-white);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
i {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
@include position-center(xy);
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
z-index: 1;
|
||||
|
||||
@include position-center(xy);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,68 @@
|
||||
<script setup name="PageHeader">
|
||||
<script lang="ts" setup name="PageHeader">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="main">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="content">
|
||||
<slot name="content">{{ content }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="main">
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot name="content">
|
||||
{{ content }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 20px;
|
||||
background-color: var(--g-app-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
.sub {
|
||||
flex: none;
|
||||
|
||||
.content {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,84 +1,89 @@
|
||||
<script setup name="PageMain">
|
||||
<script lang="ts" setup name="PageMain">
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
collaspe: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
collaspe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const titleSlot = !!useSlots().title
|
||||
|
||||
const collaspeData = ref(props.collaspe)
|
||||
function unCollaspe() {
|
||||
collaspeData.value = false
|
||||
collaspeData.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'page-main': true,
|
||||
'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">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-down" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<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">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-down" />
|
||||
</el-icon>
|
||||
</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);
|
||||
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,131 +1,137 @@
|
||||
<script setup name="PcasCascader">
|
||||
<script lang="ts" setup name="PcasCascader">
|
||||
// 行政区划数据来源于 https://github.com/modood/Administrative-divisions-of-China
|
||||
import pcasRaw from './pcas-code.json'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'pca'
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'code'
|
||||
}
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as () => 'pc' | 'pca' | 'pcas',
|
||||
default: 'pca',
|
||||
},
|
||||
format: {
|
||||
type: String as () => 'code' | 'name' | 'both',
|
||||
default: 'code',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
interface pcasItem {
|
||||
code: string
|
||||
name: string
|
||||
children?: pcasItem[]
|
||||
}
|
||||
|
||||
const pcasData = computed(() => {
|
||||
const data = []
|
||||
// 省份
|
||||
pcasRaw.forEach(p => {
|
||||
const tempP = {
|
||||
code: p.code,
|
||||
name: p.name
|
||||
}
|
||||
const tempChildrenC = []
|
||||
// 城市
|
||||
p.children.forEach(c => {
|
||||
const tempC = {
|
||||
code: c.code,
|
||||
name: c.name
|
||||
}
|
||||
if (['pca', 'pcas'].includes(props.type)) {
|
||||
const tempChildrenA = []
|
||||
// 区县
|
||||
c.children.forEach(a => {
|
||||
const tempA = {
|
||||
code: a.code,
|
||||
name: a.name
|
||||
}
|
||||
if (props.type === 'pcas') {
|
||||
const tempChildrenS = []
|
||||
// 街道
|
||||
a.children.forEach(s => {
|
||||
const tempS = {
|
||||
code: s.code,
|
||||
name: s.name
|
||||
}
|
||||
tempChildrenS.push(tempS)
|
||||
})
|
||||
tempA.children = tempChildrenS
|
||||
}
|
||||
tempChildrenA.push(tempA)
|
||||
})
|
||||
tempC.children = tempChildrenA
|
||||
}
|
||||
tempChildrenC.push(tempC)
|
||||
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)
|
||||
})
|
||||
tempP.children = tempChildrenC
|
||||
data.push(tempP)
|
||||
tempC.children = tempChildrenA
|
||||
}
|
||||
tempChildrenC.push(tempC)
|
||||
})
|
||||
return data
|
||||
tempP.children = tempChildrenC
|
||||
data.push(tempP)
|
||||
})
|
||||
return data
|
||||
})
|
||||
|
||||
const myValue = computed({
|
||||
// 将入参数据转成 code 码
|
||||
get: () => {
|
||||
return anyToCode(props.modelValue)
|
||||
},
|
||||
// 将 code 码转成出参数据
|
||||
set: value => {
|
||||
emit('update:modelValue', value ? codeToAny(value) : [])
|
||||
}
|
||||
// 将入参数据转成 code 码
|
||||
get: () => {
|
||||
return anyToCode(props.modelValue)
|
||||
},
|
||||
// 将 code 码转成出参数据
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value ? codeToAny(value) : [])
|
||||
},
|
||||
})
|
||||
|
||||
function anyToCode(value, dictionarie = pcasData.value) {
|
||||
const input = []
|
||||
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 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, dictionarie = pcasData.value) {
|
||||
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))
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
return output
|
||||
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" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
|
||||
<el-cascader v-model="myValue" :options="pcasData as any[]" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup name="SearchBar">
|
||||
<script lang="ts" setup name="SearchBar">
|
||||
const props = defineProps({
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
unfold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unfold: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['toggle'])
|
||||
@@ -15,57 +15,63 @@ const emit = defineEmits(['toggle'])
|
||||
const isUnfold = ref(!props.unfold)
|
||||
|
||||
watch(() => props.unfold, () => toggle(), {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
isUnfold.value = !isUnfold.value
|
||||
emit('toggle', isUnfold.value)
|
||||
isUnfold.value = !isUnfold.value
|
||||
emit('toggle', isUnfold.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<slot />
|
||||
<div v-if="showMore" class="more">
|
||||
<el-button text size="small" @click="toggle">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon :name="isUnfold ? 'ep:caret-top' : 'ep:caret-bottom'" />
|
||||
</el-icon>
|
||||
</template>
|
||||
{{ isUnfold ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<slot />
|
||||
<div v-if="showMore" class="more">
|
||||
<el-button text size="small" @click="toggle">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon :name="isUnfold ? 'ep:caret-top' : 'ep:caret-bottom'" />
|
||||
</el-icon>
|
||||
</template>
|
||||
{{ isUnfold ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
margin-bottom: -10px;
|
||||
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background-color: var(--el-fill-color);
|
||||
transition: background-color 0.3s;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:deep(.el-form) {
|
||||
margin-bottom: -10px;
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
.el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
<script setup name="SvgIcon">
|
||||
<script lang="ts" setup name="SvgIcon">
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
flip: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return ['', 'horizontal', 'vertical', 'both'].includes(value)
|
||||
},
|
||||
default: ''
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
validator(value) {
|
||||
return value >= 0 && value <= 360
|
||||
},
|
||||
default: 0
|
||||
}
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
flip: {
|
||||
type: String as () => 'horizontal' | 'vertical' | 'both' | '',
|
||||
default: '',
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const transformStyle = computed(() => {
|
||||
let style = []
|
||||
if (props.flip != '') {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
style.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
style.push('rotateX(180deg)')
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
const style = []
|
||||
if (props.flip !== '') {
|
||||
switch (props.flip) {
|
||||
case 'horizontal':
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
case 'vertical':
|
||||
style.push('rotateX(180deg)')
|
||||
break
|
||||
case 'both':
|
||||
style.push('rotateX(180deg)')
|
||||
style.push('rotateY(180deg)')
|
||||
break
|
||||
}
|
||||
if (props.rotate != 0) {
|
||||
style.push(`rotate(${props.rotate}deg)`)
|
||||
}
|
||||
return `transform: ${style.join(' ')};`
|
||||
}
|
||||
if (props.rotate !== 0)
|
||||
style.push(`rotate(${props.rotate}deg)`)
|
||||
|
||||
return `transform: ${style.join(' ')};`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" :style="transformStyle" />
|
||||
<svg v-else :style="transformStyle" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" :style="transformStyle" />
|
||||
<svg v-else :style="transformStyle" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${name}`" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<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">
|
||||
{{ pkg.version }}
|
||||
</el-descriptions-item>
|
||||
<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,65 +1,68 @@
|
||||
<script setup name="Trend">
|
||||
<script lang="ts" setup name="Trend">
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
validator: val => ['up', 'down'].includes(val),
|
||||
default: 'up'
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String as () => 'up' | 'down',
|
||||
default: 'up',
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const isUp = computed(() => {
|
||||
let isUp = props.type === 'up'
|
||||
if (props.reverse) {
|
||||
isUp = !isUp
|
||||
}
|
||||
return isUp
|
||||
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>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-top" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<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>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-top" />
|
||||
</el-icon>
|
||||
</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);
|
||||
.el-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--el-transition-color);
|
||||
|
||||
&.up {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: var(--el-color-danger);
|
||||
|
||||
.el-icon {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.3s;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Vendored
+266
@@ -0,0 +1,266 @@
|
||||
import type { StyleValue } from "vue"
|
||||
import type { RouteRecordRaw, RouteMeta, NavigationGuard } from 'vue-router'
|
||||
|
||||
type RecursiveRequired<T> = {
|
||||
[P in keyof T]-?: RecursiveRequired<T[P]>
|
||||
}
|
||||
|
||||
declare namespace Settings {
|
||||
interface app {
|
||||
/**
|
||||
* 颜色方案
|
||||
* @默认值 `''` 跟随系统
|
||||
* @可选值 `'light'` 明亮模式
|
||||
* @可选值 `'dark'` 暗黑模式
|
||||
*/
|
||||
colorScheme?: '' | 'light' | 'dark'
|
||||
/**
|
||||
* Element Plus 组件默认尺寸
|
||||
* @默认值 `'default'` 默认
|
||||
* @可选值 `'large'` 大号
|
||||
* @可选值 `'small'` 小号
|
||||
*/
|
||||
elementSize?: 'default' | 'large' | 'small'
|
||||
/**
|
||||
* 是否将 iconify 图标集中的 Element Plus 图标下载到本地,以便在离线环境下使用
|
||||
* @默认值 `false`
|
||||
*/
|
||||
iconifyOfflineUse?: boolean
|
||||
/**
|
||||
* 是否开启权限功能
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enablePermission?: boolean
|
||||
/**
|
||||
* 是否开启载入进度条
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enableProgress?: boolean
|
||||
/**
|
||||
* 是否开启动态标题
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableDynamicTitle?: boolean
|
||||
/**
|
||||
* 路由数据来源
|
||||
* @默认值 `'frontend'` 前端
|
||||
* @可选值 `'backend'` 后端
|
||||
* @可选值 `'filesystem'` 文件系统
|
||||
*/
|
||||
routeBaseOn?: 'frontend' | 'backend' | 'filesystem'
|
||||
}
|
||||
interface dashboard {
|
||||
/**
|
||||
* 是否开启控制台页面
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 控制台名称
|
||||
* @默认值 `'控制台'`
|
||||
*/
|
||||
title?: string
|
||||
}
|
||||
interface layout {
|
||||
/**
|
||||
* 是否开启移动端适配,开启后当页面宽度小于 992px 时自动切换为移动端展示
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableMobileAdaptation?: boolean
|
||||
}
|
||||
interface menu {
|
||||
/**
|
||||
* 导航栏数据来源,当 `app.routeBaseOn: 'filesystem'` 时生效
|
||||
* @默认值 `'frontend'` 前端
|
||||
* @可选值 `'backend'` 后端
|
||||
*/
|
||||
baseOn?: 'frontend' | 'backend'
|
||||
/**
|
||||
* 导航栏模式
|
||||
* @默认值 `'side'` 侧边栏模式(有主导航)
|
||||
* @可选值 `'head'` 顶部模式
|
||||
* @可选值 `'single'` 侧边栏模式(无主导航)
|
||||
*/
|
||||
menuMode?: 'side' | 'head' | 'single'
|
||||
/**
|
||||
* 切换主导航是否跳转页面
|
||||
* @默认值 `false`
|
||||
*/
|
||||
switchMainMenuAndPageJump?: boolean
|
||||
/**
|
||||
* 次导航是否只保持一个子项的展开
|
||||
* @默认值 `true`
|
||||
*/
|
||||
subMenuUniqueOpened?: boolean
|
||||
/**
|
||||
* 次导航是否收起
|
||||
* @默认值 `false`
|
||||
*/
|
||||
subMenuCollapse?: boolean
|
||||
/**
|
||||
* 是否开启次导航的展开/收起按钮
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableSubMenuCollapseButton?: boolean
|
||||
/**
|
||||
* 是否开启主导航切换快捷键
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
interface topbar {
|
||||
/**
|
||||
* 是否固定顶栏
|
||||
* @默认值 `true`
|
||||
*/
|
||||
fixed?: boolean
|
||||
}
|
||||
interface toolbar {
|
||||
/**
|
||||
* 是否开启全屏
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableFullscreen?: boolean
|
||||
/**
|
||||
* 是否开启页面刷新
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enablePageReload?: boolean
|
||||
/**
|
||||
* 是否开启颜色主题
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableColorScheme?: boolean
|
||||
/**
|
||||
* 是否开启应用配置,强烈建议在生产环境中关闭
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableAppSetting?: boolean
|
||||
}
|
||||
interface breadcrumb {
|
||||
/**
|
||||
* 是否开启面包屑导航
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
}
|
||||
interface navSearch {
|
||||
/**
|
||||
* 是否开启导航搜索
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 是否开启导航搜索快捷键
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
interface copyright {
|
||||
/**
|
||||
* 是否开启底部版权,同时在路由 meta 对象里可以单独设置某个路由是否显示底部版权信息
|
||||
* @默认值 `true`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 网站运行日期
|
||||
* @默认值 `'2020-2022'`
|
||||
*/
|
||||
dates?: string
|
||||
/**
|
||||
* 公司名称
|
||||
* @默认值 `'Fantastic-admin'`
|
||||
*/
|
||||
company?: string
|
||||
/**
|
||||
* 网站地址
|
||||
* @默认值 `'https://hooray.gitee.io/fantastic-admin/'`
|
||||
*/
|
||||
website?: string
|
||||
/**
|
||||
* 网站备案号
|
||||
* @默认值 `''`
|
||||
*/
|
||||
beian?: string
|
||||
}
|
||||
interface all {
|
||||
/** 应用设置 */
|
||||
app?: app
|
||||
/** 控制台设置 */
|
||||
dashboard?: dashboard
|
||||
/** 布局设置 */
|
||||
layout?: layout
|
||||
/** 导航栏设置 */
|
||||
menu?: menu
|
||||
/** 顶栏设置 */
|
||||
topbar?: topbar
|
||||
/** 工具栏设置 */
|
||||
toolbar?: toolbar
|
||||
/** 面包屑导航设置 */
|
||||
breadcrumb?: breadcrumb
|
||||
/** 导航搜索设置 */
|
||||
navSearch?: navSearch
|
||||
/** 底部版权设置 */
|
||||
copyright?: copyright
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title: string | Function
|
||||
i18n?: string
|
||||
icon?: string
|
||||
activeIcon?: string
|
||||
defaultOpened?: boolean
|
||||
auth?: string | string[]
|
||||
sidebar?: boolean
|
||||
breadcrumb?: boolean
|
||||
activeMenu?: string
|
||||
cache?: boolean | string | string[]
|
||||
breadcrumbNeste?: Route.breadcrumb[]
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Route {
|
||||
interface recordRaw extends Omit<RouteRecordRaw, 'meta' | 'children'> {
|
||||
meta: RouteMeta
|
||||
children?: recordRaw[]
|
||||
}
|
||||
interface recordMainRaw {
|
||||
meta: {
|
||||
title?: string | Function
|
||||
icon?: string
|
||||
auth?: string | string[]
|
||||
}
|
||||
children: recordRaw[]
|
||||
}
|
||||
interface breadcrumb {
|
||||
path: string
|
||||
title: string | Function
|
||||
hide: boolean
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Menu {
|
||||
/** 原始 */
|
||||
interface recordRaw {
|
||||
path?: string
|
||||
meta: {
|
||||
title: string
|
||||
icon?: string
|
||||
defaultOpened?: boolean
|
||||
auth?: string | string[]
|
||||
sidebar?: boolean
|
||||
}
|
||||
children?: recordRaw[]
|
||||
}
|
||||
/** 主导航 */
|
||||
interface recordMainRaw {
|
||||
meta: {
|
||||
title?: string
|
||||
icon?: string
|
||||
auth?: string | string[]
|
||||
}
|
||||
children: recordRaw[]
|
||||
}
|
||||
}
|
||||
@@ -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,6 @@
|
||||
import { addCollection } from '@iconify/vue'
|
||||
|
||||
export async function downloadAndInstall() {
|
||||
const data = Object.freeze(await fetch('./icons/icons.json').then(r => r.json()))
|
||||
addCollection(data)
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
<script setup name="AppSetting">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import globalSettings from '@/settings'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settings = ref(globalSettings)
|
||||
|
||||
watch(() => settings, () => {
|
||||
settingsStore.updateThemeSetting(settings.value)
|
||||
menuStore.setActived(0)
|
||||
settings.value.menu.menuMode !== 'single' && menuStore.setActived(route.fullPath)
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-theme-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
watch(copied, val => {
|
||||
if (val) {
|
||||
ElMessage.success('复制成功,请粘贴到 src/settings.custom.json 文件中!')
|
||||
}
|
||||
})
|
||||
|
||||
function handleCopy() {
|
||||
copy(JSON.stringify(settings.value, null, 4))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="应用配置" direction="rtl" :size="360">
|
||||
<el-alert title="应用配置可实时预览效果,但仅是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,将配置粘贴到 src/settings.custom.json 中即可,或者也可在 src/settings.js 中直接修改默认配置。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
|
||||
<el-divider>颜色主题</el-divider>
|
||||
<div class="color-scheme">
|
||||
<div class="switch" :class="settings.app.colorScheme" @click="settings.app.colorScheme = settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-show="settings.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settings.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</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': settings.menu.menuMode === 'side'}" @click="settings.menu.menuMode = 'side'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部模式" placement="top" :show-after="500">
|
||||
<div class="mode mode-head" :class="{'active': settings.menu.menuMode === 'head'}" @click="settings.menu.menuMode = 'head'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-single" :class="{'active': settings.menu.menuMode === 'single'}" @click="settings.menu.menuMode = 'single'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider>导航栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否折叠</div>
|
||||
<el-switch v-model="settings.menu.subMenuCollapse" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
切换跳转
|
||||
<el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settings.menu.menuMode)" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
保持展开一个
|
||||
<el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuUniqueOpened" />
|
||||
</div>
|
||||
<el-divider>顶栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否固定</div>
|
||||
<el-switch v-model="settings.topbar.fixed" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">折叠按钮</div>
|
||||
<el-switch v-model="settings.topbar.enableSidebarCollapse" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">面包屑导航</div>
|
||||
<el-switch v-model="settings.topbar.enableBreadcrumb" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
导航栏搜索
|
||||
<el-tooltip content="对导航栏进行快捷搜索" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableNavSearch" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableFullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enablePageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.enableColorScheme" />
|
||||
</div>
|
||||
<el-divider>底部版权</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否启用</div>
|
||||
<el-switch v-model="settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">日期</div>
|
||||
<el-input v-model="settings.copyright.dates" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">公司</div>
|
||||
<el-input v-model="settings.copyright.company" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">网址</div>
|
||||
<el-input v-model="settings.copyright.website" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">备案</div>
|
||||
<el-input v-model="settings.copyright.beian" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<el-divider>控制台</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否开启
|
||||
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.dashboard.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">控制台名称</div>
|
||||
<el-input v-model="settings.dashboard.title" size="small" />
|
||||
</div>
|
||||
<el-divider>其它</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
组件尺寸
|
||||
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="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>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">是否启用权限</div>
|
||||
<el-switch v-model="settings.app.enablePermission" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
载入进度条
|
||||
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableProgress" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
动态标题
|
||||
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<el-button type="primary" @click="handleCopy">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:document-copy" />
|
||||
</el-icon>
|
||||
</template>
|
||||
复制配置
|
||||
</el-button>
|
||||
</template>
|
||||
</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);
|
||||
}
|
||||
: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
.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);
|
||||
}
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary);
|
||||
}
|
||||
&::before,
|
||||
&::after,
|
||||
.mode-container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mode-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
&-side {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 40px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
&-head {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
height: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.mode-container {
|
||||
top: 20px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
&-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;
|
||||
}
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
i {
|
||||
margin-left: 4px;
|
||||
font-size: 17px;
|
||||
color: var(--el-color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
.el-switch {
|
||||
height: auto;
|
||||
}
|
||||
.el-input {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,170 +0,0 @@
|
||||
<script setup name="Header">
|
||||
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 { useMenu } from '@/utils/composables'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const navRef = ref()
|
||||
|
||||
// 顶部模式鼠标滚动
|
||||
function handlerMouserScroll(event) {
|
||||
navRef.value.scrollBy({
|
||||
left: (event.deltaY || event.detail) > 0 ? 50 : -50
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="header">
|
||||
<header v-if="settingsStore.mode === 'pc' && settingsStore.menu.menuMode === 'head'">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<!-- 顶部模式 -->
|
||||
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
|
||||
<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="useMenu().switchTo(index)">
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span v-if="item.meta.title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</header>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: var(--g-header-height);
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
.header-container {
|
||||
width: var(--g-header-width);
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
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;
|
||||
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);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.item-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: initial;
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 5px;
|
||||
width: 80px;
|
||||
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);
|
||||
}
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
&.active .item {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.tools) {
|
||||
padding: 0;
|
||||
.buttons .item .el-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;
|
||||
}
|
||||
.header-enter-from,
|
||||
.header-leave-to {
|
||||
transform: translateY(calc(var(--g-header-height) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup name="Logo">
|
||||
import imgLogo from '@/assets/images/logo.png'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
defineProps({
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const title = ref(import.meta.env.VITE_APP_TITLE)
|
||||
const logo = ref(imgLogo)
|
||||
|
||||
const to = computed(() => {
|
||||
let rtn = {}
|
||||
if (settingsStore.dashboard.enable) {
|
||||
rtn.name = 'dashboard'
|
||||
}
|
||||
return rtn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="to" class="title" :class="{'is-link': settingsStore.dashboard.enable}" :title="title">
|
||||
<img v-if="showLogo" :src="logo" class="logo">
|
||||
<span v-if="showTitle">{{ title }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--g-sidebar-logo-height);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
&.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
& + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script setup name="MainSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import { useMenu } from '@/utils/composables'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="main-sidebar">
|
||||
<div v-if="settingsStore.menu.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.menu.menuMode !== 'single')" class="main-sidebar-container">
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in menuStore.allMenus">
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" :key="index" :class="{
|
||||
'item': true,
|
||||
'active': index === menuStore.actived
|
||||
}" :title="item.meta.title" @click="useMenu().switchTo(index)"
|
||||
>
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--g-main-sidebar-width);
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
.sidebar-logo {
|
||||
transition: 0.3s;
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
.nav {
|
||||
width: inherit;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: 60px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
&:hover {
|
||||
color: var(--g-main-sidebar-menu-hover-color);
|
||||
background-color: var(--g-main-sidebar-menu-hover-bg);
|
||||
}
|
||||
&.active {
|
||||
color: var(--g-main-sidebar-menu-active-color);
|
||||
background-color: var(--g-main-sidebar-menu-active-bg);
|
||||
}
|
||||
.el-icon {
|
||||
margin: 0 auto;
|
||||
font-size: 24px;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 主侧边栏动画
|
||||
.main-sidebar-enter-active,
|
||||
.main-sidebar-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.main-sidebar-enter-from,
|
||||
.main-sidebar-leave-to {
|
||||
transform: translateX(calc(var(--g-main-sidebar-width) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -1,398 +0,0 @@
|
||||
<script setup name="Search">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { deepClone, isExternalLink } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const routeStore = useRouteStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
const searchInput = ref('')
|
||||
const sourceList = ref([])
|
||||
const actived = ref(-1)
|
||||
|
||||
const inputRef = ref()
|
||||
const resultRef = ref()
|
||||
const resultItemRef = ref([])
|
||||
const setResultItemRef = el => resultItemRef.value.push(el)
|
||||
onBeforeUpdate(() => {
|
||||
resultItemRef.value = []
|
||||
})
|
||||
|
||||
const resultList = computed(() => {
|
||||
let result = []
|
||||
result = sourceList.value.filter(item => {
|
||||
let flag = false
|
||||
if (item.title.indexOf(searchInput.value) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.path.indexOf(searchInput.value) >= 0) {
|
||||
flag = true
|
||||
}
|
||||
if (item.breadcrumb.some(b => b.indexOf(searchInput.value) >= 0)) {
|
||||
flag = true
|
||||
}
|
||||
return flag
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
watch(() => isShow.value, val => {
|
||||
if (val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
resultRef.value.scrollTop = 0
|
||||
// 当搜索显示的时候绑定上、下、回车快捷键,隐藏的时候再解绑。另外当 input 处于 focus 状态时,采用 vue 来绑定键盘事件
|
||||
hotkeys('up', keyUp)
|
||||
hotkeys('down', keyDown)
|
||||
hotkeys('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
inputRef.value.focus()
|
||||
}, 500)
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
hotkeys.unbind('up', keyUp)
|
||||
hotkeys.unbind('down', keyDown)
|
||||
hotkeys.unbind('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInput.value = ''
|
||||
actived.value = -1
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
watch(() => resultList.value, () => {
|
||||
actived.value = -1
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-search-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
hotkeys('alt+s', e => {
|
||||
if (settingsStore.topbar.enableNavSearch) {
|
||||
e.preventDefault()
|
||||
isShow.value = true
|
||||
}
|
||||
})
|
||||
hotkeys('esc', e => {
|
||||
if (settingsStore.topbar.enableNavSearch) {
|
||||
e.preventDefault()
|
||||
isShow.value = false
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.routes.map(item => {
|
||||
getSourceList(item.children)
|
||||
})
|
||||
} else {
|
||||
menuStore.menus.map(item => {
|
||||
getSourceList(item.children)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function hasChildren(item) {
|
||||
let flag = true
|
||||
if (item.children) {
|
||||
if (item.children.every(i => i.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
}
|
||||
function getSourceList(arr) {
|
||||
arr.map(item => {
|
||||
if (item.meta.sidebar !== false) {
|
||||
if (hasChildren(item)) {
|
||||
let baseBreadcrumb = item.meta.baseBreadcrumb ? deepClone(item.meta.baseBreadcrumb) : []
|
||||
baseBreadcrumb.push(item.meta.title)
|
||||
let child = deepClone(item.children)
|
||||
child.map(c => {
|
||||
c.meta.baseIcon = item.meta.icon || item.meta.baseIcon
|
||||
c.meta.baseBreadcrumb = baseBreadcrumb
|
||||
c.meta.basePath = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
|
||||
})
|
||||
getSourceList(child)
|
||||
} else {
|
||||
let breadcrumb = []
|
||||
if (item.meta.baseBreadcrumb) {
|
||||
breadcrumb = deepClone(item.meta.baseBreadcrumb)
|
||||
}
|
||||
breadcrumb.push(item.meta.title)
|
||||
let path = ''
|
||||
if (isExternalLink(item.path)) {
|
||||
path = item.path
|
||||
} else {
|
||||
path = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
|
||||
}
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || item.meta.baseIcon,
|
||||
title: item.meta.title,
|
||||
i18n: item.meta.i18n,
|
||||
breadcrumb: breadcrumb,
|
||||
path: path
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
function keyUp() {
|
||||
if (resultList.value.length) {
|
||||
actived.value -= 1
|
||||
if (actived.value < 0) {
|
||||
actived.value = resultList.value.length - 1
|
||||
}
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyDown() {
|
||||
if (resultList.value.length) {
|
||||
actived.value += 1
|
||||
if (actived.value > resultList.value.length - 1) {
|
||||
actived.value = 0
|
||||
}
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyEnter() {
|
||||
if (actived.value !== -1) {
|
||||
resultItemRef.value[actived.value].click()
|
||||
}
|
||||
}
|
||||
function handleScroll() {
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
scrollTo = resultRef.value.scrollTop
|
||||
const activedOffsetTop = resultItemRef.value[actived.value].offsetTop
|
||||
const activedClientHeight = resultItemRef.value[actived.value].clientHeight
|
||||
const searchScrollTop = resultRef.value.scrollTop
|
||||
const searchClientHeight = resultRef.value.clientHeight
|
||||
if (activedOffsetTop + activedClientHeight > searchScrollTop + searchClientHeight) {
|
||||
scrollTo = activedOffsetTop + activedClientHeight - searchClientHeight
|
||||
} else if (activedOffsetTop <= searchScrollTop) {
|
||||
scrollTo = activedOffsetTop
|
||||
}
|
||||
}
|
||||
resultRef.value.scrollTo({
|
||||
top: scrollTo
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="search" :class="{'searching': isShow}" @click="isShow && eventBus.emit('global-search-toggle')">
|
||||
<div class="container">
|
||||
<div class="search-box" @click.stop>
|
||||
<el-input ref="inputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" clearable @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="tips">
|
||||
<div class="tip">
|
||||
<span>Alt</span>+<span>S</span>
|
||||
唤醒搜索面板
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-up" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-down" />
|
||||
</el-icon>
|
||||
</span>
|
||||
切换搜索结果
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-enter" />
|
||||
</el-icon>
|
||||
</span>
|
||||
访问页面
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>ESC</span>
|
||||
退出
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="resultRef" class="result" :class="{'mobile': settingsStore.mode === 'mobile'}">
|
||||
<router-link v-for="(item, index) in resultList" :key="item.path" v-slot="{ href, navigate }" custom :to="isShow ? item.path : ''">
|
||||
<a :ref="setResultItemRef" :href="isExternalLink(item.path) ? item.path : href" class="item" :class="{'actived': index === actived}" :target="isExternalLink(item.path) ? '_blank' : '_self'" @click="navigate" @mouseover="actived = index">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-if="item.icon" :name="item.icon" />
|
||||
</el-icon>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="breadcrumb">
|
||||
<span v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex">
|
||||
{{ bc }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="path">{{ item.path }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.searching {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
.container {
|
||||
transform: initial;
|
||||
filter: initial;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
transform: scale(1.1);
|
||||
filter: blur(10px);
|
||||
.search-box {
|
||||
margin: 50px 20px 40px;
|
||||
:deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
}
|
||||
:deep(.el-input__icon) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 20px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
span {
|
||||
margin: 0 5px;
|
||||
padding: 3px 8px 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: inset 0 -2px var(--el-fill-color-lighter), inset 0 0 1px 1px var(--el-fill-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
.result {
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
max-height: calc(100% - 250px);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
box-shadow: 0 0 0 1px var(--el-border-color-darker);
|
||||
&.mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&.actived {
|
||||
background-color: var(--el-bg-color-page);
|
||||
.icon {
|
||||
color: var(--el-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.info {
|
||||
border-left-color: var(--el-border-color);
|
||||
.title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
flex: 0 0 66px;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
padding: 5px 10px 7px;
|
||||
transition: all 0.3s;
|
||||
@include text-overflow(1, true);
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
.breadcrumb,
|
||||
.path {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.3s;
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
.breadcrumb {
|
||||
span {
|
||||
margin-right: 5px;
|
||||
&:last-child i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<script setup name="SidebarItem">
|
||||
import SidebarItem from './index.vue'
|
||||
import { isExternalLink, resolveRoutePath } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
let flag = true
|
||||
if (props.item.children) {
|
||||
if (props.item.children.every(item => item.meta.sidebar === false)) {
|
||||
flag = false
|
||||
}
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-item">
|
||||
<el-sub-menu v-if="item.path == undefined" :title="item.meta.title" :index="JSON.stringify(item)">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<router-link v-else-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path)">
|
||||
<a :href="isExternalLink(resolveRoutePath(basePath, item.path)) ? resolveRoutePath(basePath, item.path) : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternalLink(resolveRoutePath(basePath, item.path)) ? '_blank' : '_self'" @click="navigate">
|
||||
<el-menu-item :title="item.meta.title" :index="resolveRoutePath(basePath, item.path)">
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
</router-link>
|
||||
<el-sub-menu v-else :title="item.meta.title" :index="resolveRoutePath(basePath, item.path)">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path)" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-menu-item span.title),
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-sub-menu__title span.title) {
|
||||
flex: 1;
|
||||
vertical-align: inherit;
|
||||
@include text-overflow;
|
||||
}
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:deep(.el-sub-menu),
|
||||
:deep(.el-menu-item) {
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
font-size: 20px;
|
||||
vertical-align: -0.25em;
|
||||
transition: transform 0.3s;
|
||||
color: unset;
|
||||
}
|
||||
.title-icon + .title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&:hover > .title-icon,
|
||||
.el-sub-menu__title:hover > .title-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
> .badge {
|
||||
&-dot {
|
||||
right: 40px;
|
||||
}
|
||||
&-text {
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--inline {
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color) !important;
|
||||
background: transparent !important;
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
.el-menu-item.is-active,
|
||||
.el-menu--collapse .el-sub-menu.is-active > .el-sub-menu__title,
|
||||
.el-sub-menu .el-menu--inline .el-menu-item.is-active {
|
||||
color: var(--g-sub-sidebar-menu-active-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg) !important;
|
||||
.icon,
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<script setup name="SubSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import SidebarItem from '../SidebarItem/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const sidebarScrollTop = ref(0)
|
||||
|
||||
function onSidebarScroll(e) {
|
||||
sidebarScrollTop.value = e.target.scrollTop
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="['side', 'head', 'single'].includes(settingsStore.menu.menuMode) || settingsStore.mode === 'mobile'" class="sub-sidebar-container" :class="{'is-collapse': settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse}" @scroll="onSidebarScroll">
|
||||
<Logo
|
||||
:show-logo="settingsStore.menu.menuMode === 'single'" :class="{
|
||||
'sidebar-logo': true,
|
||||
'sidebar-logo-bg': settingsStore.menu.menuMode === 'single',
|
||||
'shadow': sidebarScrollTop
|
||||
}"
|
||||
/>
|
||||
<!-- 侧边栏模式(无主导航) -->
|
||||
<el-menu
|
||||
:unique-opened="settingsStore.menu.subMenuUniqueOpened" :default-openeds="menuStore.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse" :collapse-transition="false" :class="{
|
||||
'is-collapse-without-logo': settingsStore.menu.menuMode !== 'single' && settingsStore.menu.subMenuCollapse
|
||||
}"
|
||||
>
|
||||
<transition-group name="sub-sidebar">
|
||||
<template v-for="(route, index) in menuStore.sidebarMenus">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path || index" :item="route" :base-path="route.path" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: var(--g-sub-sidebar-width);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
box-shadow: 10px 0 10px -10px var(--g-box-shadow-color);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow), left 0.3s, width 0.3s;
|
||||
&.is-collapse {
|
||||
width: 64px;
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
:deep(span) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebar-logo {
|
||||
transition: box-shadow 0.2s, background-color 0.3s, color 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
&:not(.sidebar-logo-bg) {
|
||||
:deep(span) {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
}
|
||||
}
|
||||
&.sidebar-logo-bg {
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
transition: border-color 0.3s, background-color 0.3s, color 0.3s, padding-top 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
&:not(.el-menu--collapse) {
|
||||
width: inherit;
|
||||
}
|
||||
&.is-collapse-without-logo {
|
||||
padding-top: 0;
|
||||
}
|
||||
&.el-menu--collapse {
|
||||
:deep(.title-icon) {
|
||||
margin-right: 0;
|
||||
}
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
span,
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.menu-radius:not(.el-menu--collapse) {
|
||||
.sidebar-item {
|
||||
padding: 0 10px;
|
||||
&:first-child {
|
||||
padding-top: 10px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
:deep(.el-menu--inline),
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 次侧边栏动画
|
||||
.sub-sidebar-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.sub-sidebar-enter-from,
|
||||
.sub-sidebar-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) skewY(10deg);
|
||||
}
|
||||
.sub-sidebar-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,168 +0,0 @@
|
||||
<script setup name="Tools">
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { useMainPage } from '@/utils/composables'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
function userCommand(command) {
|
||||
switch (command) {
|
||||
case 'dashboard':
|
||||
router.push({
|
||||
name: 'dashboard'
|
||||
})
|
||||
break
|
||||
case 'setting':
|
||||
router.push({
|
||||
name: 'personalSetting'
|
||||
})
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout().then(() => {
|
||||
router.push({
|
||||
name: 'login'
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
function pro() {
|
||||
window.open('https://hooray.gitee.io/fantastic-admin-pro-example/', 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools">
|
||||
<div class="buttons">
|
||||
<span v-if="settingsStore.mode == 'pc'" class="item item-pro" @click="pro">
|
||||
<el-icon>
|
||||
<svg-icon name="pro" />
|
||||
</el-icon>
|
||||
<span class="title">查看专业版</span>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableNavSearch" class="item" @click="eventBus.emit('global-search-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.mode === 'pc' && settingsStore.topbar.enableFullscreen" class="item" @click="toggle">
|
||||
<el-icon>
|
||||
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enablePageReload" class="item" @click="useMainPage().reload()">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:refresh-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableColorScheme" class="item" @click="settingsStore.setColorScheme(settingsStore.app.colorScheme === 'dark' ? 'light' : 'dark')">
|
||||
<el-icon>
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.topbar.enableAppSetting" class="item" @click="eventBus.emit('global-theme-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:setting" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-dropdown class="user-container" size="default" @command="userCommand">
|
||||
<div class="user-wrapper">
|
||||
<el-avatar size="small">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:user-filled" />
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
{{ userStore.account }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-bottom" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="user-dropdown">
|
||||
<el-dropdown-item v-if="settingsStore.dashboard.enable" command="dashboard">控制台</el-dropdown-item>
|
||||
<el-dropdown-item command="setting">个人设置</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
.buttons {
|
||||
margin-right: 20px;
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
width: 34px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
}
|
||||
.item-pro {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 0 10px;
|
||||
transform-origin: right center;
|
||||
animation: pro-text 3s ease-out infinite;
|
||||
@keyframes pro-text {
|
||||
0%,
|
||||
20% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50%,
|
||||
70% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.el-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.title {
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background-image: linear-gradient(to right, #ffa237, #fc455d);
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.user-container) {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
.user-wrapper {
|
||||
.el-avatar {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup name="Topbar">
|
||||
import { compile } from 'path-to-regexp'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import Tools from '../Tools/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const enableSidebarCollapse = computed(() => {
|
||||
return settingsStore.mode === 'mobile' || (
|
||||
['side', 'head', 'single'].includes(settingsStore.menu.menuMode) &&
|
||||
settingsStore.topbar.enableSidebarCollapse
|
||||
)
|
||||
})
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
let breadcrumbList = []
|
||||
if (settingsStore.dashboard.enable) {
|
||||
breadcrumbList.push({
|
||||
path: '/dashboard',
|
||||
title: settingsStore.dashboard.title
|
||||
})
|
||||
}
|
||||
if (route.meta.breadcrumbNeste) {
|
||||
breadcrumbList.push(...route.meta.breadcrumbNeste.filter(item => item.hide === false))
|
||||
}
|
||||
return breadcrumbList
|
||||
})
|
||||
|
||||
const scrollTop = ref(0)
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
function onScroll() {
|
||||
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop
|
||||
}
|
||||
|
||||
function pathCompile(path) {
|
||||
let toPath = compile(path)
|
||||
return toPath(route.params)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="topbar-container" :class="{
|
||||
'fixed': settingsStore.topbar.fixed,
|
||||
'shadow': scrollTop
|
||||
}" data-fixed-calc-width
|
||||
>
|
||||
<div class="left-box">
|
||||
<div v-if="enableSidebarCollapse" class="sidebar-collapse" :class="{'is-collapse': settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<el-icon>
|
||||
<svg-icon name="toolbar-collapse" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-breadcrumb v-if="settingsStore.topbar.enableBreadcrumb && settingsStore.mode === 'pc' && settingsStore.app.routeBaseOn !== 'filesystem'">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar-container {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--g-topbar-height);
|
||||
background-color: var(--g-toolbar-bg);
|
||||
transition: width 0.3s, top 0.3s, transform 0.3s, background-color 0.3s, var(--el-transition-box-shadow);
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 50px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
.sidebar-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color), var(--el-transition-md-fade);
|
||||
}
|
||||
&:hover .el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-collapse .el-icon {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
& + .el-breadcrumb {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
:deep(.el-breadcrumb) {
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
.el-breadcrumb__item {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
span {
|
||||
font-weight: normal;
|
||||
}
|
||||
&:last-child span {
|
||||
color: #97a8be;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 面包屑动画
|
||||
.breadcrumb-enter-active {
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) skewX(-50deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,208 +0,0 @@
|
||||
<script setup name="Layout">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Header from './components/Header/index.vue'
|
||||
import MainSidebar from './components/MainSidebar/index.vue'
|
||||
import SubSidebar from './components/SubSidebar/index.vue'
|
||||
import Topbar from './components/Topbar/index.vue'
|
||||
import Search from './components/Search/index.vue'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import { useMainPage } from '@/utils/composables'
|
||||
|
||||
const routeInfo = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const showCopyright = computed(() => {
|
||||
return typeof routeInfo.meta.copyright !== 'undefined' ? routeInfo.meta.copyright : settingsStore.copyright.enable
|
||||
})
|
||||
|
||||
watch(() => settingsStore.menu.subMenuCollapse, val => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
if (!val) {
|
||||
document.querySelector('body').classList.add('hidden')
|
||||
} else {
|
||||
document.querySelector('body').classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => routeInfo.path, () => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch(state => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', e => {
|
||||
if (settingsStore.topbar.enablePageReload) {
|
||||
e.preventDefault()
|
||||
useMainPage().reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div id="app-main">
|
||||
<Header />
|
||||
<div class="wrapper">
|
||||
<div class="sidebar-container" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse}">
|
||||
<MainSidebar />
|
||||
<SubSidebar />
|
||||
</div>
|
||||
<div class="sidebar-mask" :class="{'show': settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()" />
|
||||
<div class="main-container" :style="{'padding-bottom': $route.meta.paddingBottom}">
|
||||
<Topbar v-if="!(settingsStore.menu.menuMode === 'head' && !settingsStore.topbar.enableSidebarCollapse && !settingsStore.topbar.enableBreadcrumb)" />
|
||||
<div class="main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="main" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveStore.list">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<Copyright v-if="showCopyright" />
|
||||
</div>
|
||||
</div>
|
||||
<el-backtop :right="20" :bottom="20" title="回到顶部" />
|
||||
</div>
|
||||
<Search />
|
||||
<AppSetting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode="mobile"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc((var(--g-main-sidebar-width) + var(--g-sub-sidebar-width)) * -1));
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
&[data-menu-mode="single"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc(var(--g-sub-sidebar-width) * -1));
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: -1px 0 0 0 var(--g-box-shadow-color);
|
||||
transition: padding-top 0.3s;
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
z-index: 1010;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
transition: transform 0.3s, top 0.3s;
|
||||
width: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
.sidebar-mask {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.main-sidebar-container:not(.main-sidebar-leave-active) + .sub-sidebar-container {
|
||||
left: var(--g-main-sidebar-width);
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
margin-left: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
background-color: var(--g-main-bg);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
.topbar-container {
|
||||
top: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.topbar-container + .main {
|
||||
margin: var(--g-topbar-height) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
header:not(.header-leave-active) + .wrapper {
|
||||
padding-top: var(--g-header-height);
|
||||
.sidebar-container {
|
||||
top: var(--g-header-height);
|
||||
:deep(.sidebar-logo) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.el-menu) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
.topbar-container {
|
||||
top: var(--g-header-height);
|
||||
:deep(.tools) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区动画
|
||||
.main-enter-active {
|
||||
transition: 0.2s;
|
||||
}
|
||||
.main-leave-active {
|
||||
transition: 0.15s;
|
||||
}
|
||||
.main-enter-from {
|
||||
opacity: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.main-leave-to {
|
||||
opacity: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,532 @@
|
||||
<script lang="ts" setup name="AppSetting">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import globalSettingsDefault from '@/settings.default'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settings = ref(globalSettingsDefault)
|
||||
|
||||
watch(() => settings, () => {
|
||||
settingsStore.updateSettings(settings.value)
|
||||
menuStore.setActived(0)
|
||||
settings.value.menu.menuMode !== 'single' && menuStore.setActived(route.fullPath)
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-theme-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
|
||||
watch(copied, (val) => {
|
||||
if (val)
|
||||
ElMessage.success('复制成功,请粘贴到 src/settings.ts 文件中!')
|
||||
})
|
||||
|
||||
function handleCopy() {
|
||||
copy(JSON.stringify(settings.value, null, 4))
|
||||
}
|
||||
</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="settings.app.colorScheme" @click="settings.app.colorScheme = settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
|
||||
<el-icon class="icon">
|
||||
<svg-icon :name="settings.app.colorScheme === 'light' ? 'ep:sunny' : 'ep:moon'" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</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: settings.menu.menuMode === 'side' }" @click="settings.menu.menuMode = 'side'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="顶部模式" placement="top" :show-after="500">
|
||||
<div class="mode mode-head" :class="{ active: settings.menu.menuMode === 'head' }" @click="settings.menu.menuMode = 'head'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
|
||||
<div class="mode mode-single" :class="{ active: settings.menu.menuMode === 'single' }" @click="settings.menu.menuMode = 'single'">
|
||||
<div class="mode-container" />
|
||||
<el-icon>
|
||||
<svg-icon name="ep:check" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider>导航栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
主导航切换跳转
|
||||
<el-tooltip content="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settings.menu.menuMode)" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航保持展开一个
|
||||
<el-tooltip content="开启该功能后,次导航只保持单个菜单的展开" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuUniqueOpened" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
次导航是否折叠
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.subMenuCollapse" />
|
||||
</div>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
显示次导航折叠按钮
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.enableSubMenuCollapseButton" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.menu.menuMode)" />
|
||||
</div>
|
||||
<el-divider>顶栏</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否固定
|
||||
</div>
|
||||
<el-switch v-model="settings.topbar.fixed" />
|
||||
</div>
|
||||
<el-divider>工具栏</el-divider>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
|
||||
<div class="label">
|
||||
全屏
|
||||
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.toolbar.enableFullscreen" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
页面刷新
|
||||
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.toolbar.enablePageReload" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
颜色主题
|
||||
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="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="settings.breadcrumb.enable" />
|
||||
</div>
|
||||
<el-divider>导航搜索</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
<el-tooltip content="对导航进行快捷搜索" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.navSearch.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<el-switch v-model="settings.navSearch.enableHotkeys" :disabled="!settings.navSearch.enable" />
|
||||
</div>
|
||||
<el-divider>底部版权</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<el-switch v-model="settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
日期
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.dates" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
公司
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.company" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
网址
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.website" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
备案
|
||||
</div>
|
||||
<el-input v-model="settings.copyright.beian" size="small" :disabled="!settings.copyright.enable" />
|
||||
</div>
|
||||
<el-divider>控制台</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否开启
|
||||
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.dashboard.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
控制台名称
|
||||
</div>
|
||||
<el-input v-model="settings.dashboard.title" size="small" />
|
||||
</div>
|
||||
<el-divider>其它</el-divider>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
组件尺寸
|
||||
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="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>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用权限
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enablePermission" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
载入进度条
|
||||
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableProgress" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
动态标题
|
||||
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:question-filled" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch v-model="settings.app.enableDynamicTitle" />
|
||||
</div>
|
||||
<template v-if="isSupported" #footer>
|
||||
<el-button type="primary" @click="handleCopy">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:document-copy" />
|
||||
</el-icon>
|
||||
</template>
|
||||
复制配置
|
||||
</el-button>
|
||||
</template>
|
||||
</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);
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-mode {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after,
|
||||
.mode-container {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mode-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&-side {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 40px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&-head {
|
||||
&::before {
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
height: 10px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
width: 15px;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 20px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&-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;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
top: 5px;
|
||||
left: 25px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-left: 4px;
|
||||
font-size: 17px;
|
||||
color: var(--el-color-warning);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup name="Header">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
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.menu.menuMode === 'head'">
|
||||
<div class="header-container">
|
||||
<div class="main">
|
||||
<Logo />
|
||||
<!-- 顶部模式 -->
|
||||
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
|
||||
<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)">
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span v-if="item.meta.title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</header>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
header {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: var(--g-header-height);
|
||||
color: var(--g-header-color);
|
||||
background-color: var(--g-header-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
.header-container {
|
||||
width: var(--g-header-width);
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
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;
|
||||
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);
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: initial;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 5px;
|
||||
width: 80px;
|
||||
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);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
word-break: break-all;
|
||||
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
|
||||
&.active .item {
|
||||
color: var(--g-header-menu-active-color);
|
||||
background-color: var(--g-header-menu-active-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.tools) {
|
||||
padding: 0;
|
||||
|
||||
.buttons .item .el-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;
|
||||
}
|
||||
|
||||
.header-enter-from,
|
||||
.header-leave-to {
|
||||
transform: translateY(calc(var(--g-header-height) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-hotkeys-intro-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="isShow" title="快捷键介绍" direction="rtl" :size="360">
|
||||
<el-descriptions title="全局" :column="1" border>
|
||||
<el-descriptions-item label="查看系统信息">
|
||||
Alt + I
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys" label="唤起导航搜索">
|
||||
Alt + S
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions v-if="settingsStore.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.menu.menuMode)" title="主导航" :column="1" border>
|
||||
<el-descriptions-item label="激活下一个主导航">
|
||||
Alt + `
|
||||
</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;
|
||||
|
||||
.el-descriptions__label {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup name="Logo">
|
||||
import imgLogo from '@/assets/images/logo.png'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
defineProps({
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const title = ref(import.meta.env.VITE_APP_TITLE)
|
||||
const logo = ref(imgLogo)
|
||||
|
||||
const to = computed(() => {
|
||||
const rtn: {
|
||||
name?: string
|
||||
} = {}
|
||||
if (settingsStore.dashboard.enable)
|
||||
rtn.name = 'dashboard'
|
||||
|
||||
return rtn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="to" class="title" :class="{ 'is-link': settingsStore.dashboard.enable }" :title="title">
|
||||
<img v-if="showLogo" :src="logo" class="logo">
|
||||
<span v-if="showTitle">{{ title }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--g-sidebar-logo-height);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
&.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
& + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup name="MainSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const { switchTo } = useMenu()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="main-sidebar">
|
||||
<div v-if="settingsStore.menu.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.menu.menuMode !== 'single')" class="main-sidebar-container">
|
||||
<Logo :show-title="false" class="sidebar-logo" />
|
||||
<!-- 侧边栏模式(含主导航) -->
|
||||
<div class="nav">
|
||||
<template v-for="(item, index) in menuStore.allMenus">
|
||||
<div
|
||||
v-if="item.children && item.children.length !== 0" :key="index" class="item" :class="{
|
||||
active: index === menuStore.actived,
|
||||
}" :title="item.meta.title" @click="switchTo(index)"
|
||||
>
|
||||
<el-icon>
|
||||
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--g-main-sidebar-width);
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
.sidebar-logo {
|
||||
transition: 0.3s;
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: inherit;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: 60px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
color: var(--g-main-sidebar-menu-color);
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
transition: background-color 0.3s, var(--el-transition-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--g-main-sidebar-menu-hover-color);
|
||||
background-color: var(--g-main-sidebar-menu-hover-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--g-main-sidebar-menu-active-color);
|
||||
background-color: var(--g-main-sidebar-menu-active-bg);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin: 0 auto;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
@include text-overflow(1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主侧边栏动画
|
||||
.main-sidebar-enter-active,
|
||||
.main-sidebar-leave-active {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.main-sidebar-enter-from,
|
||||
.main-sidebar-leave-to {
|
||||
transform: translateX(calc(var(--g-main-sidebar-width) * -1));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,448 @@
|
||||
<script lang="ts" setup name="Search">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { isExternalLink } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import type { Menu, Route } from '@/global'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const routeStore = useRouteStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
interface listTypes {
|
||||
icon?: string
|
||||
title: string
|
||||
breadcrumb: string[]
|
||||
path: string
|
||||
}
|
||||
|
||||
const isShow = ref(false)
|
||||
const searchInput = ref('')
|
||||
const sourceList = ref<listTypes[]>([])
|
||||
const actived = ref(-1)
|
||||
|
||||
const searchInputRef = ref()
|
||||
const searchResultRef = ref()
|
||||
const searchResultItemRef = ref<any>([])
|
||||
const setSearchResultItemRef = (el: any) => searchResultItemRef.value.push(el)
|
||||
onBeforeUpdate(() => {
|
||||
searchResultItemRef.value = []
|
||||
})
|
||||
|
||||
const resultList = computed(() => {
|
||||
let result = []
|
||||
result = sourceList.value.filter((item) => {
|
||||
let flag = false
|
||||
if (item.title.includes(searchInput.value))
|
||||
flag = true
|
||||
|
||||
if (item.path.includes(searchInput.value))
|
||||
flag = true
|
||||
|
||||
if (item.breadcrumb.some(b => b.includes(searchInput.value)))
|
||||
flag = true
|
||||
|
||||
return flag
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
watch(() => isShow.value, (val) => {
|
||||
if (val) {
|
||||
document.body.classList.add('hidden')
|
||||
searchResultRef.value.scrollTop = 0
|
||||
// 当搜索显示的时候绑定上、下、回车快捷键,隐藏的时候再解绑。另外当 input 处于 focus 状态时,采用 vue 来绑定键盘事件
|
||||
hotkeys('up', keyUp)
|
||||
hotkeys('down', keyDown)
|
||||
hotkeys('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInputRef.value.focus()
|
||||
}, 500)
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('hidden')
|
||||
hotkeys.unbind('up', keyUp)
|
||||
hotkeys.unbind('down', keyDown)
|
||||
hotkeys.unbind('enter', keyEnter)
|
||||
setTimeout(() => {
|
||||
searchInput.value = ''
|
||||
actived.value = -1
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
watch(() => resultList.value, () => {
|
||||
actived.value = -1
|
||||
searchResultItemRef.value = []
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('global-search-toggle', () => {
|
||||
isShow.value = !isShow.value
|
||||
})
|
||||
hotkeys('alt+s', (e) => {
|
||||
if (settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
isShow.value = true
|
||||
}
|
||||
})
|
||||
hotkeys('esc', (e) => {
|
||||
if (settingsStore.navSearch.enable && settingsStore.navSearch.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
isShow.value = false
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.routes.forEach((item) => {
|
||||
item.children && getSourceList(item.children)
|
||||
})
|
||||
}
|
||||
else {
|
||||
menuStore.menus.forEach((item) => {
|
||||
getSourceListByMenus(item.children)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function hasChildren(item: Route.recordRaw) {
|
||||
let flag = true
|
||||
if (item.children && item.children.every(i => i.meta.sidebar === false))
|
||||
flag = false
|
||||
|
||||
return flag
|
||||
}
|
||||
function getSourceList(arr: Route.recordRaw[], basePath?: string, icon?: string, breadcrumb?: string[]) {
|
||||
arr.forEach((item) => {
|
||||
if (item.meta.sidebar !== false) {
|
||||
const breadcrumbTemp = cloneDeep(breadcrumb) || []
|
||||
if (item.children && hasChildren(item)) {
|
||||
breadcrumbTemp.push(typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title)
|
||||
getSourceList(item.children, basePath ? [basePath, item.path].join('/') : item.path, item.meta.icon || icon, breadcrumbTemp)
|
||||
}
|
||||
else {
|
||||
breadcrumbTemp.push(typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title)
|
||||
let path = ''
|
||||
if (isExternalLink(item.path))
|
||||
path = item.path
|
||||
|
||||
else
|
||||
path = basePath ? [basePath, item.path].join('/') : item.path
|
||||
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || icon,
|
||||
title: typeof item.meta.title === 'function' ? item.meta.title() : item.meta.title,
|
||||
breadcrumb: breadcrumbTemp,
|
||||
path,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
function getSourceListByMenus(arr: Menu.recordRaw[], icon?: string, breadcrumb?: string[]) {
|
||||
arr.forEach((item) => {
|
||||
const breadcrumbTemp = cloneDeep(breadcrumb) || []
|
||||
if (item.children && item.children.length > 0) {
|
||||
breadcrumbTemp.push(item.meta.title)
|
||||
getSourceListByMenus(item.children, item.meta.icon, breadcrumbTemp)
|
||||
}
|
||||
else {
|
||||
breadcrumbTemp.push(item.meta.title)
|
||||
sourceList.value.push({
|
||||
icon: item.meta.icon || icon,
|
||||
title: item.meta.title,
|
||||
path: item.path as string,
|
||||
breadcrumb: breadcrumbTemp,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function keyUp() {
|
||||
if (resultList.value.length) {
|
||||
actived.value -= 1
|
||||
if (actived.value < 0)
|
||||
actived.value = resultList.value.length - 1
|
||||
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyDown() {
|
||||
if (resultList.value.length) {
|
||||
actived.value += 1
|
||||
if (actived.value > resultList.value.length - 1)
|
||||
actived.value = 0
|
||||
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
function keyEnter() {
|
||||
if (actived.value !== -1)
|
||||
searchResultItemRef.value[actived.value].click()
|
||||
}
|
||||
function handleScroll() {
|
||||
let scrollTo = 0
|
||||
if (actived.value !== -1) {
|
||||
scrollTo = searchResultRef.value.scrollTop
|
||||
const activedOffsetTop = searchResultItemRef.value[actived.value].offsetTop
|
||||
const activedClientHeight = searchResultItemRef.value[actived.value].clientHeight
|
||||
const searchScrollTop = searchResultRef.value.scrollTop
|
||||
const searchClientHeight = searchResultRef.value.clientHeight
|
||||
if (activedOffsetTop + activedClientHeight > searchScrollTop + searchClientHeight)
|
||||
scrollTo = activedOffsetTop + activedClientHeight - searchClientHeight
|
||||
|
||||
else if (activedOffsetTop <= searchScrollTop)
|
||||
scrollTo = activedOffsetTop
|
||||
}
|
||||
searchResultRef.value.scrollTo({
|
||||
top: scrollTo,
|
||||
})
|
||||
}
|
||||
|
||||
function pageJump(url: string) {
|
||||
if (isExternalLink(url))
|
||||
window.open(url, 'top')
|
||||
|
||||
else
|
||||
router.push(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="search" :class="{ searching: isShow }" @click="isShow && eventBus.emit('global-search-toggle')">
|
||||
<div class="container">
|
||||
<div class="search-box" @click.stop>
|
||||
<el-input ref="searchInputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" clearable @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="settingsStore.mode === 'pc'" class="tips">
|
||||
<div class="tip">
|
||||
<span>Alt</span>+<span>S</span>
|
||||
唤醒搜索面板
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-up" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-down" />
|
||||
</el-icon>
|
||||
</span>
|
||||
切换搜索结果
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>
|
||||
<el-icon>
|
||||
<svg-icon name="search-enter" />
|
||||
</el-icon>
|
||||
</span>
|
||||
访问页面
|
||||
</div>
|
||||
<div class="tip">
|
||||
<span>ESC</span>
|
||||
退出
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="searchResultRef" class="result" :class="{ mobile: settingsStore.mode === 'mobile' }">
|
||||
<a v-for="(item, index) in resultList" :key="item.path" :ref="setSearchResultItemRef" class="item" :class="{ actived: index === actived }" @click="pageJump(item.path)" @mouseover="actived = index">
|
||||
<el-icon class="icon">
|
||||
<svg-icon v-if="item.icon" :name="item.icon" />
|
||||
</el-icon>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="breadcrumb">
|
||||
<span v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex">
|
||||
{{ bc }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:arrow-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="path">{{ item.path }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search {
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.searching {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
.container {
|
||||
transform: initial;
|
||||
filter: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
transform: scale(1.1);
|
||||
filter: blur(10px);
|
||||
|
||||
.search-box {
|
||||
margin: 50px 20px 40px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
}
|
||||
|
||||
:deep(.el-input__icon) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 20px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
|
||||
span {
|
||||
margin: 0 5px;
|
||||
padding: 3px 8px 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color);
|
||||
box-shadow: inset 0 -2px var(--el-fill-color-lighter), inset 0 0 1px 1px var(--el-fill-color-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
max-height: calc(100% - 250px);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
box-shadow: 0 0 0 1px var(--el-border-color-darker);
|
||||
|
||||
&.mobile {
|
||||
max-height: calc(100% - 200px);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.actived {
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
.icon {
|
||||
color: var(--el-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.info {
|
||||
border-left-color: var(--el-border-color);
|
||||
|
||||
.title {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.breadcrumb,
|
||||
.path {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 0 66px;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
font-size: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
padding: 5px 10px 7px;
|
||||
transition: all 0.3s;
|
||||
|
||||
@include text-overflow(1, true);
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
|
||||
.breadcrumb,
|
||||
.path {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.3s;
|
||||
|
||||
@include text-overflow(1, true);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
span {
|
||||
margin-right: 5px;
|
||||
|
||||
&:last-child i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts" setup name="SidebarItem">
|
||||
// import SidebarItem from './index.vue'
|
||||
import { isExternalLink, resolveRoutePath } from '@/utils'
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as () => Menu.recordRaw,
|
||||
required: true,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
let flag = true
|
||||
if (props.item.children) {
|
||||
if (props.item.children.every(item => item.meta.sidebar === false))
|
||||
flag = false
|
||||
}
|
||||
else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-item">
|
||||
<router-link v-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path || '')">
|
||||
<a :href="isExternalLink(resolveRoutePath(basePath, item.path || '')) ? resolveRoutePath(basePath, item.path || '') : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternalLink(resolveRoutePath(basePath, item.path || '')) ? '_blank' : '_self'" @click="navigate">
|
||||
<el-menu-item :title="item.meta.title" :index="resolveRoutePath(basePath, item.path || '')">
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</a>
|
||||
</router-link>
|
||||
<el-sub-menu v-else :title="item.meta.title" :index="resolveRoutePath(basePath, item.path || '')">
|
||||
<template #title>
|
||||
<el-icon v-if="item.meta.icon" class="title-icon">
|
||||
<svg-icon :name="item.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-for="route in item.children">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path || '')" />
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-menu-item span.title),
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-sub-menu__title span.title) {
|
||||
flex: 1;
|
||||
vertical-align: inherit;
|
||||
|
||||
@include text-overflow;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu),
|
||||
:deep(.el-menu-item) {
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
font-size: 20px;
|
||||
vertical-align: -0.25em;
|
||||
transition: transform 0.3s;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.title-icon + .title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:hover > .title-icon,
|
||||
.el-sub-menu__title:hover > .title-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
> .badge {
|
||||
&-dot {
|
||||
right: 40px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu--inline {
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
background-color: var(--g-sub-sidebar-menu-bg) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--g-sub-sidebar-menu-color) !important;
|
||||
background: transparent !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--g-sub-sidebar-menu-hover-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-menu--collapse .el-sub-menu.is-active > .el-sub-menu__title,
|
||||
.el-sub-menu .el-menu--inline .el-menu-item.is-active {
|
||||
color: var(--g-sub-sidebar-menu-active-color) !important;
|
||||
background-color: var(--g-sub-sidebar-menu-active-bg) !important;
|
||||
|
||||
.icon,
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup name="SubSidebar">
|
||||
import Logo from '../Logo/index.vue'
|
||||
import SidebarItem from '../SidebarItem/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const sidebarScrollTop = ref(0)
|
||||
|
||||
function onSidebarScroll(e: Event) {
|
||||
sidebarScrollTop.value = (e.target as HTMLElement).scrollTop
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="['side', 'head', 'single'].includes(settingsStore.menu.menuMode) || settingsStore.mode === 'mobile'" class="sub-sidebar-container" :class="{ 'is-collapse': settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse }" @scroll="onSidebarScroll">
|
||||
<Logo
|
||||
:show-logo="settingsStore.menu.menuMode === 'single'" class="sidebar-logo" :class="{
|
||||
'sidebar-logo-bg': settingsStore.menu.menuMode === 'single',
|
||||
'shadow': sidebarScrollTop,
|
||||
}"
|
||||
/>
|
||||
<!-- 侧边栏模式(无主导航) -->
|
||||
<el-menu
|
||||
:unique-opened="settingsStore.menu.subMenuUniqueOpened" :default-openeds="menuStore.defaultOpenedPaths" :default-active="$route.meta.activeMenu || $route.path" :collapse="settingsStore.mode === 'pc' && settingsStore.menu.subMenuCollapse" :collapse-transition="false" :class="{
|
||||
'is-collapse-without-logo': settingsStore.menu.menuMode !== 'single' && settingsStore.menu.subMenuCollapse,
|
||||
}"
|
||||
>
|
||||
<transition-group name="sub-sidebar">
|
||||
<template v-for="(route, index) in menuStore.sidebarMenus">
|
||||
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path || index" :item="route" :base-path="route.path" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-sidebar-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: var(--g-sub-sidebar-width);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
box-shadow: 10px 0 10px -10px var(--g-box-shadow-color);
|
||||
transition: background-color 0.3s, var(--el-transition-box-shadow), left 0.3s, width 0.3s;
|
||||
|
||||
&.is-collapse {
|
||||
width: 64px;
|
||||
|
||||
.sidebar-logo {
|
||||
&:not(.sidebar-logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(span) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
transition: box-shadow 0.2s, background-color 0.3s, color 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
|
||||
&:not(.sidebar-logo-bg) {
|
||||
:deep(span) {
|
||||
color: var(--g-sub-sidebar-menu-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.sidebar-logo-bg {
|
||||
background-color: var(--g-main-sidebar-bg);
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: 0;
|
||||
padding-top: var(--g-sidebar-logo-height);
|
||||
transition: border-color 0.3s, background-color 0.3s, color 0.3s, padding-top 0.3s;
|
||||
background-color: var(--g-sub-sidebar-bg);
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&.is-collapse-without-logo {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.el-menu--collapse {
|
||||
:deep(.title-icon) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
span,
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-radius:not(.el-menu--collapse) {
|
||||
.sidebar-item {
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu--inline),
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 次侧边栏动画
|
||||
.sub-sidebar-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.sub-sidebar-enter-from,
|
||||
.sub-sidebar-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) skewY(10deg);
|
||||
}
|
||||
|
||||
.sub-sidebar-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script lang="ts" setup name="Tools">
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const mainPage = useMainPage()
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
function userCommand(command: 'dashboard' | 'setting' | 'hotkeys' | 'logout') {
|
||||
switch (command) {
|
||||
case 'dashboard':
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
})
|
||||
break
|
||||
case 'setting':
|
||||
router.push({
|
||||
name: 'personalSetting',
|
||||
})
|
||||
break
|
||||
case 'hotkeys':
|
||||
eventBus.emit('global-hotkeys-intro-toggle')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout().then(() => {
|
||||
router.push({
|
||||
name: 'login',
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
function pro() {
|
||||
window.open('https://hooray.gitee.io/fantastic-admin-pro-example/', 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools">
|
||||
<div class="buttons">
|
||||
<span v-if="settingsStore.navSearch.enable" class="item" @click="eventBus.emit('global-search-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:search" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.mode === 'pc' && settingsStore.toolbar.enableFullscreen" class="item" @click="toggle">
|
||||
<el-icon>
|
||||
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enablePageReload" class="item" @click="mainPage.reload()">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:refresh-right" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enableColorScheme" class="item" @click="settingsStore.setColorScheme(settingsStore.app.colorScheme === 'dark' ? 'light' : 'dark')">
|
||||
<el-icon>
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'light'" name="ep:sunny" />
|
||||
<svg-icon v-show="settingsStore.app.colorScheme === 'dark'" name="ep:moon" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-if="settingsStore.toolbar.enableAppSetting" class="item" @click="eventBus.emit('global-theme-toggle')">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:setting" />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<el-dropdown class="user-container" size="default" @command="userCommand">
|
||||
<div class="user-wrapper">
|
||||
<el-avatar size="small">
|
||||
<el-icon>
|
||||
<svg-icon name="ep:user-filled" />
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
{{ userStore.account }}
|
||||
<el-icon>
|
||||
<svg-icon name="ep:caret-bottom" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="user-dropdown">
|
||||
<el-dropdown-item v-if="settingsStore.dashboard.enable" command="dashboard">
|
||||
控制台
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="setting">
|
||||
个人设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="settingsStore.mode === 'pc'" divided command="hotkeys">
|
||||
快捷键介绍
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
.buttons {
|
||||
margin-right: 20px;
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
width: 34px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.user-container) {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
.user-wrapper {
|
||||
.el-avatar {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup name="Topbar">
|
||||
import { compile } from 'path-to-regexp'
|
||||
import Tools from '../Tools/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const enableSubMenuCollapseButton = computed(() => {
|
||||
return settingsStore.mode === 'mobile' || (
|
||||
['side', 'head', 'single'].includes(settingsStore.menu.menuMode)
|
||||
&& settingsStore.menu.enableSubMenuCollapseButton
|
||||
)
|
||||
})
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
const breadcrumbList = []
|
||||
if (settingsStore.dashboard.enable) {
|
||||
breadcrumbList.push({
|
||||
path: '/dashboard',
|
||||
title: settingsStore.dashboard.title,
|
||||
})
|
||||
}
|
||||
if (route.meta.breadcrumbNeste)
|
||||
breadcrumbList.push(...route.meta.breadcrumbNeste.filter(item => item.hide === false))
|
||||
|
||||
return breadcrumbList
|
||||
})
|
||||
|
||||
const scrollTop = ref(0)
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
})
|
||||
function onScroll() {
|
||||
scrollTop.value = document.documentElement.scrollTop || document.body.scrollTop
|
||||
}
|
||||
|
||||
function pathCompile(path: string) {
|
||||
const toPath = compile(path)
|
||||
return toPath(route.params)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="topbar-container" :class="{
|
||||
fixed: settingsStore.topbar.fixed,
|
||||
shadow: scrollTop,
|
||||
}" data-fixed-calc-width
|
||||
>
|
||||
<div class="left-box">
|
||||
<div v-if="enableSubMenuCollapseButton" class="sidebar-collapse" :class="{ 'is-collapse': settingsStore.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()">
|
||||
<el-icon>
|
||||
<svg-icon name="toolbar-collapse" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-breadcrumb v-if="settingsStore.breadcrumb.enable && settingsStore.mode === 'pc' && settingsStore.app.routeBaseOn !== 'filesystem'">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<Tools />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar-container {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--g-topbar-height);
|
||||
background-color: var(--g-toolbar-bg);
|
||||
transition: width 0.3s, top 0.3s, transform 0.3s, background-color 0.3s, var(--el-transition-box-shadow);
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 50px;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
|
||||
|
||||
.sidebar-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--el-transition-color), var(--el-transition-md-fade);
|
||||
}
|
||||
|
||||
&:hover .el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-collapse .el-icon {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
|
||||
& + .el-breadcrumb {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb) {
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-breadcrumb__item {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
|
||||
span {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:last-child span {
|
||||
color: #97a8be;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 面包屑动画
|
||||
.breadcrumb-enter-active {
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) skewX(-50deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<script lang="ts" setup name="Layout">
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Header from './components/Header/index.vue'
|
||||
import MainSidebar from './components/MainSidebar/index.vue'
|
||||
import SubSidebar from './components/SubSidebar/index.vue'
|
||||
import Topbar from './components/Topbar/index.vue'
|
||||
import Search from './components/Search/index.vue'
|
||||
import AppSetting from './components/AppSetting/index.vue'
|
||||
import HotkeysIntro from './components/HotkeysIntro/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
|
||||
const routeInfo = useRoute()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
watch(() => settingsStore.menu.subMenuCollapse, (val) => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
if (!val)
|
||||
document.body.classList.add('hidden')
|
||||
|
||||
else
|
||||
document.body.classList.remove('hidden')
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => routeInfo.path, () => {
|
||||
if (settingsStore.mode === 'mobile') {
|
||||
settingsStore.$patch((state) => {
|
||||
state.menu.subMenuCollapse = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('f5', (e) => {
|
||||
if (settingsStore.toolbar.enablePageReload) {
|
||||
e.preventDefault()
|
||||
useMainPage().reload()
|
||||
}
|
||||
})
|
||||
hotkeys('alt+`', (e) => {
|
||||
if (settingsStore.menu.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
useMenu().switchTo(menuStore.actived + 1 < menuStore.allMenus.length ? menuStore.actived + 1 : 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('f5')
|
||||
hotkeys.unbind('alt+`')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div id="app-main">
|
||||
<Header />
|
||||
<div class="wrapper">
|
||||
<div class="sidebar-container" :class="{ show: settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse }">
|
||||
<MainSidebar />
|
||||
<SubSidebar />
|
||||
</div>
|
||||
<div class="sidebar-mask" :class="{ show: settingsStore.mode === 'mobile' && !settingsStore.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()" />
|
||||
<div class="main-container" :style="{ 'padding-bottom': $route.meta.paddingBottom } as any">
|
||||
<Topbar v-if="!(settingsStore.menu.menuMode === 'head' && !settingsStore.menu.enableSubMenuCollapseButton && !settingsStore.breadcrumb.enable)" />
|
||||
<div class="main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="main" mode="out-in" appear>
|
||||
<keep-alive :include="keepAliveStore.list">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<Copyright />
|
||||
</div>
|
||||
</div>
|
||||
<el-backtop :right="20" :bottom="20" title="回到顶部" />
|
||||
</div>
|
||||
<Search />
|
||||
<AppSetting />
|
||||
<HotkeysIntro />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
[data-mode="mobile"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc((var(--g-main-sidebar-width) + var(--g-sub-sidebar-width)) * -1));
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
&[data-menu-mode="single"] {
|
||||
.sidebar-container {
|
||||
transform: translateX(calc(var(--g-sub-sidebar-width) * -1));
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: -1px 0 0 0 var(--g-box-shadow-color);
|
||||
transition: padding-top 0.3s;
|
||||
|
||||
.sidebar-container {
|
||||
position: fixed;
|
||||
z-index: 1010;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
transition: transform 0.3s, top 0.3s;
|
||||
width: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
}
|
||||
|
||||
.sidebar-mask {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
transform: translateZ(0);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.main-sidebar-container:not(.main-sidebar-leave-active) + .sub-sidebar-container {
|
||||
left: var(--g-main-sidebar-width);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
margin-left: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
|
||||
background-color: var(--g-main-bg);
|
||||
transition: margin-left 0.3s, background-color 0.3s;
|
||||
|
||||
.topbar-container {
|
||||
top: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.topbar-container + .main {
|
||||
margin: var(--g-topbar-height) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header:not(.header-leave-active) + .wrapper {
|
||||
padding-top: var(--g-header-height);
|
||||
|
||||
.sidebar-container {
|
||||
top: var(--g-header-height);
|
||||
|
||||
:deep(.sidebar-logo) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-menu) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
.topbar-container {
|
||||
top: var(--g-header-height);
|
||||
|
||||
:deep(.tools) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区动画
|
||||
.main-enter-active {
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.main-leave-active {
|
||||
transition: 0.15s;
|
||||
}
|
||||
|
||||
.main-enter-from {
|
||||
opacity: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.main-leave-to {
|
||||
opacity: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
+12
-17
@@ -1,30 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import App from './App.vue'
|
||||
const app = createApp(App)
|
||||
|
||||
import pinia from './store'
|
||||
app.use(pinia)
|
||||
|
||||
import router from './router'
|
||||
app.use(router)
|
||||
|
||||
/* importElementPlusPlaceholder */
|
||||
import useSettingsStore from './store/modules/settings'
|
||||
|
||||
// 自定义指令
|
||||
import directive from '@/utils/directive'
|
||||
directive(app)
|
||||
|
||||
// 加载 svg 图标
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
// 加载 iconify 图标(element plus)
|
||||
import { downloadAndInstall } from '@/iconify-ep'
|
||||
import useSettingsStore from './store/modules/settings'
|
||||
if (useSettingsStore().app.iconifyOfflineUse) {
|
||||
downloadAndInstall()
|
||||
}
|
||||
|
||||
// 全局样式
|
||||
import '@/assets/styles/globals.scss'
|
||||
// 加载 iconify 图标(element plus)
|
||||
import { downloadAndInstall } from '@/iconify-ep'
|
||||
if (useSettingsStore().app.iconifyOfflineUse)
|
||||
downloadAndInstall()
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
directive(app)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,15 +0,0 @@
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
|
||||
const menu = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default menu
|
||||
@@ -0,0 +1,17 @@
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const menu: Menu.recordMainRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default',
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default menu
|
||||
@@ -1,46 +0,0 @@
|
||||
export default {
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'sidebar-menu'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Menu } from '@/global'
|
||||
|
||||
const menus: Menu.recordRaw = {
|
||||
meta: {
|
||||
title: '多级导航',
|
||||
icon: 'heroicons-solid:menu-alt-3',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/page',
|
||||
meta: {
|
||||
title: '导航1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/page',
|
||||
meta: {
|
||||
title: '导航2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page1',
|
||||
meta: {
|
||||
title: '导航2-2-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/multilevel_menu_example/level2/level3/page2',
|
||||
meta: {
|
||||
title: '导航2-2-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default menus
|
||||
@@ -1,54 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/member/login',
|
||||
method: 'post',
|
||||
response: option => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: option.body.account,
|
||||
token: '@string',
|
||||
failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/member/permission',
|
||||
method: 'get',
|
||||
response: option => {
|
||||
let permissions = []
|
||||
if (option.query.account == 'admin') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
'permission.create',
|
||||
'permission.edit',
|
||||
'permission.remove'
|
||||
]
|
||||
} else if (option.query.account == 'test') {
|
||||
permissions = [
|
||||
'permission.browse'
|
||||
]
|
||||
}
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
permissions
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/mock/member/edit/password',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
export default [
|
||||
{
|
||||
url: '/mock/member/login',
|
||||
method: 'post',
|
||||
response: (option: any) => {
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
account: option.body.account,
|
||||
token: '@string',
|
||||
failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/member/permission',
|
||||
method: 'get',
|
||||
response: (option: any) => {
|
||||
let permissions: string[] = []
|
||||
if (option.query.account === 'admin') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
'permission.create',
|
||||
'permission.edit',
|
||||
'permission.remove',
|
||||
]
|
||||
}
|
||||
else if (option.query.account === 'test') {
|
||||
permissions = [
|
||||
'permission.browse',
|
||||
]
|
||||
}
|
||||
return {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
permissions,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/mock/member/edit/password',
|
||||
method: 'post',
|
||||
response: {
|
||||
error: '',
|
||||
status: 1,
|
||||
data: {
|
||||
isSuccess: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const mocks = []
|
||||
const mockContext = import.meta.globEager('./mock/*.js')
|
||||
Object.keys(mockContext).forEach(v => {
|
||||
mocks.push(...mockContext[v].default)
|
||||
})
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mocks)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||
|
||||
const mocks: any[] = []
|
||||
const mockContext = import.meta.glob('./mock/*.ts', { eager: true })
|
||||
Object.keys(mockContext).forEach((v) => {
|
||||
mocks.push(...(mockContext[v] as any).default)
|
||||
})
|
||||
|
||||
export function setupProdMockServer() {
|
||||
createProdMockServer(mocks)
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
|
||||
import '@/assets/styles/nprogress.scss'
|
||||
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||
const { isLoading } = useNProgress()
|
||||
|
||||
// 路由相关数据
|
||||
import { constantRoutes, asyncRoutes } from './routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
router.beforeEach(async(to, from, next) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const routeStore = useRouteStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = true)
|
||||
// 是否已登录
|
||||
if (userStore.isLogin) {
|
||||
// 是否已根据权限动态生成并挂载路由
|
||||
if (routeStore.isGenerate) {
|
||||
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
|
||||
settingsStore.menu.menuMode !== 'single' && menuStore.setActived(to.path)
|
||||
if (to.name) {
|
||||
if (to.matched.length !== 0) {
|
||||
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
|
||||
if (to.name == 'login') {
|
||||
next({
|
||||
name: 'dashboard',
|
||||
replace: true
|
||||
})
|
||||
} else if (!settingsStore.dashboard.enable && to.name == 'dashboard') {
|
||||
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
|
||||
if (menuStore.sidebarMenus.length > 0) {
|
||||
next({
|
||||
path: menuStore.sidebarMenusFirstDeepestPath,
|
||||
replace: true
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
|
||||
next({
|
||||
path: '/404'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
switch (settingsStore.app.routeBaseOn) {
|
||||
case 'frontend':
|
||||
await routeStore.generateRoutesAtFront(asyncRoutes)
|
||||
break
|
||||
case 'backend':
|
||||
await routeStore.generateRoutesAtBack()
|
||||
break
|
||||
case 'filesystem':
|
||||
await routeStore.generateRoutesAtFilesystem(asyncRoutes)
|
||||
switch (settingsStore.menu.baseOn) {
|
||||
case 'frontend':
|
||||
await menuStore.generateMenusAtFront()
|
||||
break
|
||||
case 'backend':
|
||||
await menuStore.generateMenusAtBack()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
let removeRoutes = []
|
||||
routeStore.flatRoutes.forEach(route => {
|
||||
if (!/^(https?:|mailto:|tel:)/.test(route.path)) {
|
||||
removeRoutes.push(router.addRoute(route))
|
||||
}
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.flatSystemRoutes.forEach(route => {
|
||||
removeRoutes.push(router.addRoute(route))
|
||||
})
|
||||
}
|
||||
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
|
||||
routeStore.setCurrentRemoveRoutes(removeRoutes)
|
||||
next({
|
||||
path: to.fullPath,
|
||||
query: to.query,
|
||||
replace: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (to.name != 'login') {
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath
|
||||
}
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = false)
|
||||
// 设置页面 title
|
||||
to.meta.title && settingsStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
|
||||
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
|
||||
if (to.meta.cache) {
|
||||
let componentName = to.matched[to.matched.length - 1].components.default.name
|
||||
if (componentName) {
|
||||
keepAliveStore.add(componentName)
|
||||
} else {
|
||||
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
}
|
||||
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
|
||||
if (from.meta.cache) {
|
||||
let componentName = from.matched[from.matched.length - 1].components.default.name
|
||||
// 通过 meta.cache 判断针对哪些页面进行缓存
|
||||
switch (typeof from.meta.cache) {
|
||||
case 'string':
|
||||
if (from.meta.cache != to.name) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
if (!from.meta.cache.includes(to.name)) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name == 'reload') {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,164 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||
import '@/assets/styles/nprogress.scss'
|
||||
// 路由相关数据
|
||||
import { asyncRoutes, asyncRoutesByFilesystem, constantRoutes, constantRoutesByFilesystem } from './routes'
|
||||
import pinia from '@/store'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useKeepAliveStore from '@/store/modules/keepAlive'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useMenuStore from '@/store/modules/menu'
|
||||
import useRouteStore from '@/store/modules/route'
|
||||
|
||||
const { isLoading } = useNProgress()
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: useSettingsStore(pinia).app.routeBaseOn === 'filesystem' ? constantRoutesByFilesystem : constantRoutes as RouteRecordRaw[],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const routeStore = useRouteStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = true)
|
||||
// 是否已登录
|
||||
if (userStore.isLogin) {
|
||||
// 是否已根据权限动态生成并挂载路由
|
||||
if (routeStore.isGenerate) {
|
||||
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
|
||||
settingsStore.menu.menuMode !== 'single' && menuStore.setActived(to.path)
|
||||
if (to.name) {
|
||||
if (to.matched.length !== 0) {
|
||||
// 如果已登录状态下,进入登录页会强制跳转到控制台页面
|
||||
if (to.name === 'login') {
|
||||
next({
|
||||
name: 'dashboard',
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
else if (!settingsStore.dashboard.enable && to.name === 'dashboard') {
|
||||
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
|
||||
if (menuStore.sidebarMenus.length > 0) {
|
||||
next({
|
||||
path: menuStore.sidebarMenusFirstDeepestPath,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面
|
||||
next({
|
||||
path: '/404',
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (settingsStore.app.routeBaseOn) {
|
||||
case 'frontend':
|
||||
await routeStore.generateRoutesAtFront(asyncRoutes)
|
||||
break
|
||||
case 'backend':
|
||||
await routeStore.generateRoutesAtBack()
|
||||
break
|
||||
case 'filesystem':
|
||||
await routeStore.generateRoutesAtFilesystem(asyncRoutesByFilesystem)
|
||||
switch (settingsStore.menu.baseOn) {
|
||||
case 'frontend':
|
||||
await menuStore.generateMenusAtFront()
|
||||
break
|
||||
case 'backend':
|
||||
await menuStore.generateMenusAtBack()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
const removeRoutes: Function[] = []
|
||||
routeStore.flatRoutes.forEach((route) => {
|
||||
if (!/^(https?:|mailto:|tel:)/.test(route.path))
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
})
|
||||
if (settingsStore.app.routeBaseOn !== 'filesystem') {
|
||||
routeStore.flatSystemRoutes.forEach((route) => {
|
||||
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
|
||||
})
|
||||
}
|
||||
// 记录路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
|
||||
routeStore.setCurrentRemoveRoutes(removeRoutes)
|
||||
next({
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (to.name !== 'login') {
|
||||
next({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.fullPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
settingsStore.app.enableProgress && (isLoading.value = false)
|
||||
// 设置页面 title
|
||||
to.meta.title && settingsStore.setTitle(typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title)
|
||||
// 判断当前页面是否开启缓存,如果开启,则将当前页面的 name 信息存入 keep-alive 全局状态
|
||||
if (to.meta.cache) {
|
||||
const componentName = to.matched.at(-1)?.components?.default.name
|
||||
if (componentName)
|
||||
keepAliveStore.add(componentName)
|
||||
|
||||
else
|
||||
console.warn('该页面组件未设置组件名,会导致缓存失效,请检查')
|
||||
}
|
||||
// 判断离开页面是否开启缓存,如果开启,则根据缓存规则判断是否需要清空 keep-alive 全局状态里离开页面的 name 信息
|
||||
if (from.meta.cache) {
|
||||
const componentName = from.matched.at(-1)?.components?.default.name
|
||||
if (componentName) {
|
||||
// 通过 meta.cache 判断针对哪些页面进行缓存
|
||||
switch (typeof from.meta.cache) {
|
||||
case 'string':
|
||||
if (from.meta.cache !== to.name)
|
||||
keepAliveStore.remove(componentName)
|
||||
|
||||
break
|
||||
case 'object':
|
||||
if (!from.meta.cache.includes(to.name as string))
|
||||
keepAliveStore.remove(componentName)
|
||||
|
||||
break
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name === 'reload')
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
}
|
||||
document.body.scrollTop = 0
|
||||
document.documentElement.scrollTop = 0
|
||||
})
|
||||
|
||||
export default router
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user