初始化拉取,原文件名:admin-plus-webpack5
Some checks failed
Call HTTPS API / build (push) Has been cancelled

This commit is contained in:
2025-12-26 20:43:10 +08:00
parent 7c68ec3a42
commit 3e2da1115e
420 changed files with 75413 additions and 2 deletions

4
.browserslistrc Normal file
View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

104
.env Normal file
View File

@@ -0,0 +1,104 @@
# Vab Admin 系列产品受国家计算机软件著作权保护(证书号:软著登字第 7051316 号)。
# 关于举报盗版侵权请发送举报材料至我司客服邮箱1204505056@qq.com一经查实官司所得收入20%归举报人所有80%归律师事务所所有。
# Vue Admin系列产品购买地址//vuejs-core.cn/authorization
# 1.购买者可将授权后的产品用于任意「符合国家法律法规」的应用平台,禁止用于黄赌毒等危害国家安全与稳定的网站。
# 2.购买主体购买后可用于开发商业项目,不限制域名和项目数量,购买主体不可将源码分享第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 3.购买者务必尊重知识产权,严格保证不恶意传播产品源码、不得直接对授权的产品本身进行二次转售或倒卖、开源、不得对授权的产品进行简单包装后声称为自己的产品等,无论有意或无意,我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 4.购买者不可将vip群文档及资料分享给第三方否则我们有权利收回产品授权及更新权限并根据事态轻重追究相应法律责任。
# 5.购买者购买项目不可以用来构建存在竞争性质的产品并直接对外销售否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 6.购买者购买项目中的源码(包含全部源码、及部分源码片段)不可以用于任何形式的开源项目,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 7.用于公司的项目商用时购买需提供公司名称,用于证明购买过我们的项目来用于商业用途,防范法律风险,我们不会将【购买公司】信息泄漏到互联网或告知第三方。
# 8.用于个人学习需提供姓名、联系方式。
# 9.如用于外包项目购买者购买项目中的源码不可直接对外出售npm run build编译后的项目不受限制。
# 10.虚拟物品不支持退货退款。
# 11.最终解释权归vab系列著作权人所有。
# 第1步请在此处将test变更为您的github用户名请务必填写购买时绑定的github用户名同一个授权配置不同用户名会导致您的授权永久失效
VUE_GITHUB_USER_NAME=test
# 第2步请在项目根目录新建一个.env.local的新文件切记是新建空的文件不是直接拷贝.env文件的内容
# 第3步.env.local的文件只能有一行不可以换行购买时生成格式如下VUE_APP_SECRET_KEY=XXXXXXX
# 以下内容不建议修改建议将VUE_APP_SECRET_KEY配置到【.env.local】中
VUE_APP_SECRET_KEY=preview

4
.env.development Normal file
View File

@@ -0,0 +1,4 @@
# 开发环境VUE_APP_BASE_URL可以选择自己配置成需要的接口地址如"https://api.xxx.com"
# 此文件修改后需要重启项目
NODE_ENV=development
VUE_APP_BASE_URL='/vab-mock-server'

4
.env.production Normal file
View File

@@ -0,0 +1,4 @@
# 生产环境VUE_APP_BASE_URL可以选择自己配置成需要的接口地址如"https://api.xxx.com"
# 此文件修改后需要重启项目
NODE_ENV=production
VUE_APP_BASE_URL='/vab-mock-server'

4
.env.test Normal file
View File

@@ -0,0 +1,4 @@
# 测试环境VUE_APP_BASE_URL可以选择自己配置成需要的接口地址如"https://api.xxx.com"
# 此文件修改后需要重启项目
NODE_ENV=production
VUE_APP_BASE_URL='/vab-mock-server'

7
.eslintignore Normal file
View File

@@ -0,0 +1,7 @@
library/build/vuePlugins/components.d.ts
node_modules
src/assets
src/icons
public
dist
vab-icons

81
.eslintrc.js Normal file
View File

@@ -0,0 +1,81 @@
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
root: true,
env: {
node: true,
browser: true,
},
globals: {
defineOptions: 'writable',
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaVersion: 2020,
},
rules: {
'import-x/order': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-this-alias': 'off',
'array-callback-return': 'off',
'escape-case': 'off',
'eslint-comments/no-unlimited-disable': 'off',
'import/order': 'off',
'no-alert': 'off',
'no-console': 'off',
'no-debugger': 'off',
'no-restricted-imports': 'off',
'no-return-await': 'off',
'prefer-const': 'off',
'prefer-template': 'error',
'unicorn/consistent-function-scoping': 'off',
'unicorn/escape-case': 'off',
'unicorn/filename-case': 'off',
'unicorn/import-style': 'off',
'unicorn/no-abusive-eslint-disable': 'off',
'unicorn/no-array-callback-reference': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/no-nested-ternary': 'off',
'unicorn/no-null': 'off',
'unicorn/no-object-as-default-parameter': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/no-this-assignment': 'off',
'unicorn/numeric-separators-style': 'off',
'unicorn/prefer-array-some': 'off',
'unicorn/prefer-default-parameters': 'off',
'unicorn/prefer-dom-node-append': 'off',
'unicorn/prefer-dom-node-remove': 'off',
'unicorn/prefer-logical-operator-over-ternary': 'off',
'unicorn/prefer-math-trunc': 'off',
'unicorn/prefer-module': 'off',
'unicorn/prefer-number-properties': 'off',
'unicorn/prefer-query-selector': 'off',
'unicorn/prefer-spread': 'off',
'unicorn/prefer-string-slice': 'off',
'unicorn/prefer-structured-clone': 'off',
'unicorn/prefer-ternary': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/expiring-todo-comments': 'off',
'unicorn/consistent-destructuring': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-reserved-component-names': 'off',
'vue/no-setup-props-destructure': 'off',
'vue/no-v-html': 'off',
'vue/require-default-prop': 'off',
'unicorn/number-literal-case': 'off',
'@typescript-eslint/no-var-requires': 'off',
'import/first': 'off',
'object-shorthand': 'off',
'unicorn/no-console-spaces': 'off',
'unicorn/prefer-dom-node-text-content': 'off',
'unicorn/prefer-code-point': 'off',
'@typescript-eslint/consistent-type-imports': 'off',
camelcase: 'off',
},
})

17
.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
*.html text eol=lf
*.css text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.scss text eol=lf
*.vue text eol=lf
*.hbs text eol=lf
*.sh text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
.browserslistrc text eol=lf
.editorconfig text eol=lf
.eslintignore text eol=lf
.gitattributes text eol=lf
LICENSE text eol=lf
*.conf text eol=lf

11
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: Call HTTPS API
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Call HTTPS API
env:
API_ENDPOINT: https://api.vuejs-core.cn
run: |
curl -X GET "$API_ENDPOINT" -G --data "repository=$GITHUB_REPOSITORY"

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
.DS_Store
node_modules
node_modules.nosync
/dist
# local env files
.env.local
.env.*.local
# Log files
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Lock files
yarn.lock
pnpm-lock.yaml
package-lock.json
# Yarn v2 not using using Zero-Installs
.yarn/*
#!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
# Vab
public/video
*.zip
*.7z
*.rar
/.history

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
auto-imports.d.ts
components.d.ts
index.html
website.html

18
.stylelintrc.js Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
extends: [
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue',
'stylelint-config-recess-order',
],
rules: {
'no-empty-source': null,
'at-rule-no-unknown': null,
'property-no-unknown': null,
'function-no-unknown': null,
'selector-class-pattern': null,
'no-descending-specificity': null,
'scss/no-global-function-names': null,
'selector-pseudo-class-no-unknown': null,
},
ignoreFiles: ['dist/**/*', 'public/index.html'],
}

76
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,76 @@
{
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.quickSuggestions": {
"strings": true
},
"workbench.colorTheme": "One Monokai",
"editor.tabSize": 2,
"editor.detectIndentation": false,
"emmet.triggerExpansionOnTab": true,
"editor.formatOnSave": true,
"javascript.format.enable": true,
"git.enableSmartCommit": true,
"git.autofetch": true,
"git.confirmSync": false,
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"liveServer.settings.donotShowInfoMsg": true,
"explorer.confirmDelete": false,
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.updateImportsOnFileMove.enabled": "always",
"files.exclude": {
"**/.idea": true
},
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit",
"source.fixAll.eslint": "explicit"
},
"stylelint.validate": ["html", "vue", "js", "scss"],
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.suggest.snippetsPreventQuickSuggestions": false,
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.vueIndentScriptAndStyle": true,
"docthis.authorName": "github.com/zxwk1998",
"docthis.includeAuthorTag": true,
"docthis.includeDescriptionTag": true,
"docthis.enableHungarianNotationEvaluation": true,
"docthis.inferTypesFromNames": true,
"vetur.format.defaultFormatter.html": "prettier",
"files.autoSave": "onFocusChange",
"path-intellisense.mappings": {
"@": "${workspaceRoot}/src"
},
"files.eol": "\n",
"vue.codeActions.enabled": false,
"cSpell.words": [
"cnpm",
"filemanager",
"jsencrypt",
"logicflow",
"rushstack",
"taobao",
"unplugin",
"unplugins",
"vueuse",
"wangeditor",
"webpackbar"
],
"i18n-ally.localesPaths": [
"src/i18n",
"src/i18n/locales"
]
}

373
LICENSE Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

113
README.md
View File

@@ -1,3 +1,112 @@
# Web_Template_Vue3_Dev
<div align="center">
<img width="200" src="https://gcore.jsdelivr.net/gh/zxwk1998/image/logo/vab.svg" alt="VAB"/>
<h1>admin-plus</h1>
</div>
中后台前端框架 Vue3-Admin-Plus 模板项目
## 🔈 框架使用建议
- 使用前请一定先阅读 vip 群文档及群文档中的常见问题,一般在群公告前 5 条。
- 对于常见问题可直接使用 qq 群【消息记录】功能快速寻找到答案。
- 如果您经过 qq 群聊天记录、翻阅文档、百度后努力尝试仍无法解决问题,可通过 vip 群寻求帮助,讨论时间法定工作日 10 点-16 点。
- 2021 年 3 月 6 日后main 分支支持 ts、js 混合开发,建议不熟悉 ts 的用户继续使用 js熟悉 ts 用户可自行选择开发语言。
- 对于热心回答群内其他成员问题的用户,所提建议将优先被采纳,并可获得部分内测版本体验资格。
- 关于举报盗版侵权请发送举报材料至fanhuihui1998@126.com一经查实官司所得收入 20%归举报人所有80%归律师事务所所有。
- 关于客服人员满意度评价以及相关建议请发送材料至fanhuihui1998@126.com邮件标题满意度评价邮件正文评价依据我们必将认真对待每一位客户的诉求。
- 关于 bug 反馈请发送材料至fanhuihui1998@126.com邮件标题bug 反馈邮件正文bug 截图及描述。
## 🔈 框架使用约定
- 1.购买者可将授权后的产品用于任意「符合国家法律法规」的应用平台,禁止用于黄赌毒等危害国家安全与稳定的网站,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 2.购买主体购买后可用于开发商业项目,不限制域名和项目数量,购买主体不可将源码分享第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 3.购买者务必尊重知识产权,严格保证不恶意传播产品源码、不得直接对授权的产品本身进行二次转售或倒卖、开源、不得对授权的产品进行简单包装后声称为自己的产品等,无论有意或无意,我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 4.购买者不可将 vip 群文档及资料分享给第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 5.购买者购买项目不可以用来构建存在竞争性质的产品并直接对外销售否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 6.购买者购买项目中的源码包含全部源码、及部分源码片段不可以用于任何形式的开源项目不可将源码放置于码云、github等开源平台否则我们有权利收回产品授权及更新权限并根据事态轻重追究相应法律责任。
- 7.购买者用于公司的项目商用时购买必须提供公司名称,用于证明购买过我们的项目来进行商业用途,防范法律风险,我们承诺对购买公司信息信息严格保密,不会泄漏到互联网或用于产品宣传。
- 8.购买者用于个人学习需提供姓名、手机联系方式进行实名认证,如无法提供请勿下单。
- 9.如用于外包项目购买者购买项目中的源码不可直接对外出售npm run build 编译后的项目不受限制。
- 10.如果您的公司基于 Vab Admin 系列自行研发的产品(如 OA、ERP、SASS 等)需对外销售,并且产品中包含我们框架的前端源码,那么您无法购买以上版本,需联系客服购买专属定制版本(不为第三方提供前端框架代码请忽略本条)。
- 11.虚拟物品下单后不支持退货退款。
- 12.购买者需遵守以上约定,最终解释权归 vab 系列著作权人所有,如果您无法遵守以上约定,请勿下单。
```
注:以上协议以 //vuejs-core.cn/authorization/ 底部为准
```
## 🔗 链接
- 💻 常规版演示地址:[admin-plus](//vuejs-core.cn/admin-plus/)
- 📝 使用文档:(文档地址及密码请查看 vip 群群公告第一条)
- 🗃 更新日志:[Releases](https://github.com/zxwk2024/admin-plus/releases)
- 📌 付费版及 vip 群购买地址:[购买地址](//vuejs-core.cn/authorization/)
<!-- ## 🌱 版本
- `dev`分支为开发分支,较为激进,不推荐直接使用,非专业前端请勿使用。
- 对于感兴趣的提交(commit),可使用精选(Cherry-Pick)复制到自己的项目中
| 分支名 | 是否精简提交 | 是否精简功能 | 是否支持多国语言 | 同步时间 | 维护人 |
| -------------------------------------------------------------------------------- | :----------: | :----------: | :--------------: | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| [dev](https://github.com/zxwk2024/admin-plus/tree/webpack5) | ❌ | ❌ | ✔ | 即时 | All |
| [main](https://github.com/zxwk2024/admin-plus/) | ❌ | ❌ | ✔ | 10-30 天 | <a href="https://github.com/zxwk1998" target="_blank"><img style="border-radius:999px" src="https://avatars3.githubusercontent.com/u/26647258?s=50&u=753921fb23f418996dffd6196e89729fcb2329ed&v=4"/></a> |
| [release/main](https://github.com/zxwk2024/admin-plus/tree/release/main) | ✔ | ❌ | ✔ | 40-60 天 | <a href="https://github.com/FlowPeakFish" target="_blank"><img style="border-radius:999px" src="https://avatars3.githubusercontent.com/u/29328241?s=50&u=bb0977b405ccf1a101ce4e18e4fb8d958854ca60&v=4"/></a> |
| [release/template](https://github.com/zxwk2024/admin-plus/tree/release/template) | ✔ | ✔ | ✔ | 40-60 天 | <a href="https://github.com/FlowPeakFish" target="_blank"><img style="border-radius:999px" src="https://avatars3.githubusercontent.com/u/29328241?s=50&u=bb0977b405ccf1a101ce4e18e4fb8d958854ca60&v=4"/></a> |
| [release/seed](https://github.com/zxwk2024/admin-plus/tree/release/seed) | ✔ | ✔ | ❌ | 40-60 天 | <a href="https://github.com/FlowPeakFish" target="_blank"><img style="border-radius:999px" src="https://avatars3.githubusercontent.com/u/29328241?s=50&u=bb0977b405ccf1a101ce4e18e4fb8d958854ca60&v=4"/></a> | -->
## ✅ 版权须知
Vab Admin 系列产品受国家计算机软件著作权保护(证书号:软著登字第 7051316 号),
禁止公开及传播产品源文件、二次出售等,
违者将承担相应的法律责任,并影响自身使用。
## 🧑‍💻 增值服务
### vip 群
- 每位购买 Admin 的用户均可获得 1 个免费的 vip 互助群免费入群资格,可反馈 bug、协助框架问题解答无需额外购买
- 免费名额之外,额外加入 vip 群 100/人 仅限已购买框架的的公司员工加入,购买后联系 微信 zxwk-bfq 即可)
- [购买地址,网页右下角切换付款码即可](//vuejs-core.cn/authorization/)
### 定制开发
- 承接各类基于 vab 开发的前端项目
- 承接项目范围 3K+ 至 无上限
- 支持签订合同
- 支持提供发票
- 结算流程前期款50%- 中期款30%- 尾款20%
- 联系方式:见当前页底部
### 企业一对一远程培训
- 承接一对一远程培训服务(支持提供发票)
- 承接时间: 周一至周六上午 10 点 - 晚上 10 点
- 价格400 - 10000
- 承接方式:单次、包月、包年
- 联系方式:见当前页底部
### 个人一对一技术指导
- 承接时间: 周一至周六上午 10 点 - 晚上 10 点
- 价格300 - 500
- 承接方式:单日
- 支持零基础远程教学(学员需学习刻苦,有上进心)
- 学员需完成老师布置的任务
- 联系方式:见当前页底部
### 联系方式
```txt
微信客服zxwk-bfq (备注来意)
邮件标题:企业一对一远程培训 - 公司名称,定制开发 - 公司名称,一对一技术支持 - 公司名称
邮件内容:大致描述 + 联系方式 + 预估需要时间 + 预算
后续: 收到邮件后,工作人员会于第一时间回复
```

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
}

6
git.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
git config --global http.proxy http://127.0.0.1:4780;
git config --global https.proxy https://127.0.0.1:4780;
exec /bin/bash

18
jsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"baseUrl": "./",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"paths": {
"~/*": ["*"],
"@/*": ["src/*"],
"/#/*": ["types/*"],
"@vab/*": ["library/*"],
"@gp": ["library/plugins/vab"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,6 @@
module.exports = {
webpackBanner:
' build: Vue Admin' +
' Plus \n copyright: vue-admin-' +
'beautiful.com \n time: ',
}

View File

@@ -0,0 +1,12 @@
const Webpack = require('webpack')
const { webpackBanner } = require('./config.ts')
module.exports = {
createBanner: (config) => {
config
.plugin('banner')
.use(Webpack.BannerPlugin, [
`${webpackBanner}${process.env.VUE_APP_UPDATE_TIME}`,
])
},
}

View File

@@ -0,0 +1,22 @@
const dayjs = require('dayjs')
const { outputDir, abbreviation } = require('../../../../src/config')
const FileManagerPlugin = require('filemanager-webpack-plugin')
module.exports = {
createBuild7z: (config) => {
config.plugin('fileManager').use(FileManagerPlugin, [
{
events: {
onEnd: {
archive: [
{
source: `./${outputDir}`,
destination: `./${outputDir}/${abbreviation}_${dayjs().unix()}.zip`,
},
],
},
},
},
])
},
}

View File

@@ -0,0 +1,16 @@
const productionGzipExtensions = ['html', 'js', 'css', 'svg']
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
createGzip: (config) => {
config.plugin('compression').use(CompressionWebpackPlugin, [
{
filename: '[path][base].gz[query]',
algorithm: 'gzip',
test: new RegExp(`\\.(${productionGzipExtensions.join('|')})$`),
threshold: 8192,
minRatio: 0.8,
},
])
},
}

View File

@@ -0,0 +1,12 @@
module.exports = {
createImageCompression: (config) => {
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
bypassOnDebug: true,
})
.end()
},
}

View File

@@ -0,0 +1,43 @@
const { createGzip } = require('./gzip/index.ts')
const { createBanner } = require('./banner/index.ts')
const { createBuild7z } = require('./build7z/index.ts')
const { createSvgSprite } = require('./svgSprite/index.ts')
const { createOptimization } = require('./optimization/index.ts')
const { createSourceInjector } = require('./sourceInjector/index.ts')
const { createImageCompression } = require('./imageCompression/index.ts')
const { build7z, buildGzip, imageCompression } = require('../../../src/config')
const path = require('path')
module.exports = {
createChainWebpack: (env, config) => {
config.resolve.symlinks(true)
createBanner(config)
createSvgSprite(config)
if (env === 'production') {
if (build7z) createBuild7z(config)
if (buildGzip) createGzip(config)
if (imageCompression && process.env.VAB_VARIABLE !== 'website')
createImageCompression(config)
createOptimization(config)
}
if (env === 'development') config.devtool('cheap-module-source-map')
createSourceInjector(config)
// 添加一些构建优化
// 避免处理node_modules中已经编译过的文件
config.module
.rule('js')
.include.add(path.resolve('src'))
.add(path.resolve('library'))
.end()
.exclude.add(/node_modules/)
.end()
// 优化构建性能
config.plugin('fork-ts-checker').tap((options) => {
options[0].formatter = 'codeframe'
options[0].async = false
return options
})
},
}

View File

@@ -0,0 +1,74 @@
const rely = require('call-' + 'rely')
const { resolve } = require('path')
module.exports = {
createOptimization: (config) => {
process.env['VUE_AP' + 'P_RELY'] = rely
config.performance.set('hints', false)
config.optimization.splitChunks({
automaticNameDelimiter: '-',
chunks: 'all',
cacheGroups: {
// 默认缓存组
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
// 公共chunk
common: {
name: 'vab-common',
minChunks: 2,
priority: -10,
chunks: 'initial',
maxInitialRequests: 5,
minSize: 0,
},
chunk: {
name: 'vab-chunk',
test: /[\\/]node_modules[\\/]/,
minSize: 131072,
maxSize: 524288,
chunks: 'initial',
minChunks: 2,
priority: 10,
},
vue: {
name: 'vue',
test: /[\\/]node_modules[\\/](vue(.*)|core-js)[\\/]/,
chunks: 'initial',
priority: 20,
},
elementPlus: {
name: 'element-plus',
test: /[\\/]node_modules[\\/]_?element-plus(.*)/,
priority: 30,
chunks: 'all',
},
extra: {
name: 'vab-plugins',
test: resolve('src/plugins'),
priority: 40,
},
components: {
name: 'vab-components',
test: resolve('library/components'),
priority: 50,
},
xlsx: {
name: 'xlsx',
test: /[\\/]node_modules[\\/]_?xlsx(.*)/,
priority: 60,
},
echarts: {
name: 'echarts',
test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/,
priority: 65,
chunks: 'all',
},
},
})
// 配置runtimeChunk
config.optimization.runtimeChunk('single')
},
}

View File

@@ -0,0 +1,11 @@
const injector = require.resolve('./injector.ts')
module.exports = {
createSourceInjector: (config) => {
config.module
.rule('vue')
.use('vue-filename-injector')
.loader(injector)
.after('vue-loader')
},
}

View File

@@ -0,0 +1,14 @@
const { relative } = require('path')
const blockName = 'vue-filename-injector'
module.exports = function (content) {
const { rootContext, resourcePath } = this
const context = rootContext || process.cwd()
const filePath = relative(context, resourcePath).replace(/\\/g, '/')
content += `<${blockName}>
export default function (Component) {
Component.__source = ${JSON.stringify(filePath)}
}
</${blockName}>`
return content
}

View File

@@ -0,0 +1,15 @@
const { resolve } = require('path')
module.exports = {
createSvgSprite: (config) => {
config.module.rule('svg').exclude.add(resolve('src/icon'))
config.module
.rule('vabIcon')
.test(/\.svg$/)
.include.add(resolve('src/icon'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({ symbolId: 'vab-icon-[name]' })
},
}

7
library/build/index.ts Normal file
View File

@@ -0,0 +1,7 @@
const { createVuePlugin } = require('./vuePlugins/index.ts')
const { createChainWebpack } = require('./chainWebpack/index.ts')
module.exports = {
createVuePlugin,
createChainWebpack,
}

View File

@@ -0,0 +1,322 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElLoading: typeof import('element-plus/es')['ElLoading']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElNotification: typeof import('element-plus/es')['ElNotification']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const axios: typeof import('axios')['default']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
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 ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
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 makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const manualResetRef: typeof import('@vueuse/core')['manualResetRef']
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 onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
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 onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refManualReset: typeof import('@vueuse/core')['refManualReset']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
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 syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

123
library/build/vuePlugins/components.d.ts vendored Normal file
View File

@@ -0,0 +1,123 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElBadge: typeof import('element-plus/es')['ElBadge']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRate: typeof import('element-plus/es')['ElRate']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTransfer: typeof import('element-plus/es')['ElTransfer']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VabApp: typeof import('./../../components/VabApp/index.vue')['default']
VabAppMain: typeof import('./../../components/VabAppMain/index.vue')['default']
VabAvatar: typeof import('./../../components/VabAvatar/index.vue')['default']
VabBreadcrumb: typeof import('./../../components/VabBreadcrumb/index.vue')['default']
VabCard: typeof import('./../../components/VabCard/index.vue')['default']
VabColorfulCard: typeof import('./../../components/VabColorfulCard/index.vue')['default']
VabColumnBar: typeof import('./../../components/VabColumnBar/index.vue')['default']
VabErrorLog: typeof import('./../../components/VabErrorLog/index.vue')['default']
VabFold: typeof import('./../../components/VabFold/index.vue')['default']
VabFooter: typeof import('./../../components/VabFooter/index.vue')['default']
VabFullScreen: typeof import('./../../components/VabFullScreen/index.vue')['default']
VabHeader: typeof import('./../../components/VabHeader/index.vue')['default']
VabLanguage: typeof import('./../../components/VabLanguage/index.vue')['default']
VabLink: typeof import('./../../components/VabLink/index.vue')['default']
VabLock: typeof import('./../../components/VabLock/index.vue')['default']
VabLogo: typeof import('./../../components/VabLogo/index.vue')['default']
VabMenu: typeof import('./../../components/VabMenu/index.vue')['default']
VabMenuItem: typeof import('./../../components/VabMenu/components/VabMenuItem.vue')['default']
VabNav: typeof import('./../../components/VabNav/index.vue')['default']
VabNotice: typeof import('./../../components/VabNotice/index.vue')['default']
VabQueryForm: typeof import('./../../components/VabQueryForm/index.vue')['default']
VabQueryFormBottomPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormBottomPanel.vue')['default']
VabQueryFormLeftPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormLeftPanel.vue')['default']
VabQueryFormRightPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormRightPanel.vue')['default']
VabQueryFormTopPanel: typeof import('./../../components/VabQueryForm/components/VabQueryFormTopPanel.vue')['default']
VabRefresh: typeof import('./../../components/VabRefresh/index.vue')['default']
VabRouterView: typeof import('./../../components/VabRouterView/index.vue')['default']
VabSearch: typeof import('./../../components/VabSearch/index.vue')['default']
VabSideBar: typeof import('./../../components/VabSideBar/index.vue')['default']
VabSubMenu: typeof import('./../../components/VabMenu/components/VabSubMenu.vue')['default']
VabTabs: typeof import('./../../components/VabTabs/index.vue')['default']
VabTheme: typeof import('./../../components/VabTheme/index.vue')['default']
VabThemeDrawer: typeof import('./../../components/VabTheme/components/VabThemeDrawer.vue')['default']
VabThemeSetting: typeof import('./../../components/VabTheme/components/VabThemeSetting.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
createDefineOptions: () => [
require('unplugin-vue-define-options/webpack')(),
],
}

View File

@@ -0,0 +1,10 @@
// @ts-ignore
const Webpack = require('webpack')
module.exports = {
createDefinePlugin: () => [
new Webpack.DefinePlugin({
__APP_INFO__: process.env.VUE_APP_INFO,
}),
],
}

View File

@@ -0,0 +1,19 @@
const { createUnPlugin } = require('vue-' + 'unplugins')
const { createWebpackBar } = require('./webpack' + 'Bar/index.ts')
const { createDefineOptions } = require('./defineOptions/index.ts')
const { createDefinePlugin } = require('./definePlugin/index.ts')
const { createProvidePlugin } = require('./providePlugin/index.ts')
const { createMinChunkSizePlugin } = require('./minChunkSizePlugin/index.ts')
const dev = process.env.NODE_ENV === 'development'
module.exports = {
createVuePlugin: () => [
...createDefineOptions(),
...createUnPlugin(),
require('unplugin-element-plus/webpack')(),
...createWebpackBar(),
...createDefinePlugin(),
...createProvidePlugin(),
...(dev ? [] : createMinChunkSizePlugin()),
],
}

View File

@@ -0,0 +1,14 @@
// @ts-ignore
const Webpack = require('webpack')
const { buildOptimize } = require('../../../../src/config')
module.exports = {
createMinChunkSizePlugin: () =>
buildOptimize
? []
: [
new Webpack.optimize.MinChunkSizePlugin({
minChunkSize: 1024 * 300,
}),
],
}

View File

@@ -0,0 +1,7 @@
// @ts-ignore
const Webpack = require('webpack')
const { providePlugin } = require('../../../../src/config')
module.exports = {
createProvidePlugin: () => [new Webpack.ProvidePlugin(providePlugin)],
}

View File

@@ -0,0 +1,10 @@
const WebpackBar = require('webpackbar')
const { version } = require('../../../../package.json')
module.exports = {
createWebpackBar: () => [
new WebpackBar({
name: `Vue-` + `Admin` + `-Plus ${version}`,
}),
],
}

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { useHead } from '@vueuse/head'
import { pwa } from '@/config'
import { enLocale, zhLocale } from '@/i18n'
const route = useRoute()
const { locale: language } = useI18n()
const locale = computed(() =>
language.value === 'en' ? enLocale : zhLocale
)
const VabUpdate = defineAsyncComponent(
() => import('@/plugins/VabUpdate/index.vue')
)
const siteData = reactive({
description: '',
})
watchEffect(() => {
siteData.description = `${'Vue'} ${'Admin'} ${'Plus'}-${route.meta.title} - 简介、官网、首页、文档和下载 - 前端开发框架`
})
useHead({
meta: [
{
name: `description`,
content: computed(() => siteData.description),
},
],
})
</script>
<template>
<el-config-provider
:button="{
autoInsertSpace: true,
}"
:locale="locale"
>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
<vab-update v-if="pwa" ref="vabUpdateRef" />
</el-config-provider>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { handleActivePath } from '@/utils/routes'
const route = useRoute()
const routesStore: any = useRoutesStore()
const { tab, activeMenu } = storeToRefs(routesStore)
watch(
route,
() => {
if (tab.value.data !== route.matched[0].name)
tab.value.data = route.matched[0].name
activeMenu.value.data = handleActivePath(route)
},
{ immediate: true }
)
</script>
<template>
<div class="vab-app-main">
<section>
<vab-router-view />
</section>
<vab-footer />
</div>
</template>

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { toLoginRoute } from '@/utils/routes'
import { translate } from '@/i18n'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { avatar, username } = storeToRefs(userStore)
const { logout } = userStore
const active = ref(false)
const handleVisibleChange = (val: boolean) => {
active.value = val
}
const handleCommand = async (command: string) => {
switch (command) {
case 'logout':
await logout()
await router.push(toLoginRoute(route.fullPath))
break
case 'personalCenter':
await router.push('/setting/personalCenter')
break
case 'friendlyTip':
await router.push('/friendly-tip')
break
case 'shop':
await window.open('https://vuejs-core.cn/shop-vite')
break
}
}
</script>
<template>
<el-dropdown @command="handleCommand" @visible-change="handleVisibleChange">
<span class="avatar-dropdown">
<el-avatar class="user-avatar" :src="avatar" />
<div class="user-name">
<span class="hidden-xs-only" :title="username">
{{ username }}
</span>
<vab-icon
class="vab-dropdown"
:class="{ 'vab-dropdown-active': active }"
icon="arrow-down-s-line"
/>
</div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="personalCenter">
<vab-icon icon="user-line" />
<span>{{ translate('个人中心') }}</span>
</el-dropdown-item>
<el-dropdown-item command="shop">
<vab-icon icon="vuejs-line" />
<span>{{ translate('shop vite') }}</span>
</el-dropdown-item>
<el-dropdown-item command="logout">
<vab-icon icon="logout-circle-r-line" />
<span>{{ translate('退出登录') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style lang="scss" scoped>
.avatar-dropdown {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
justify-items: center;
.user-avatar {
flex-shrink: 0;
width: 40px;
height: 40px;
margin-left: 15px;
cursor: pointer;
border-radius: 50%;
}
.user-name {
position: relative;
display: flex;
flex-shrink: 0;
align-content: center;
align-items: center;
height: 40px;
margin-left: 6px;
line-height: 40px;
cursor: pointer;
span {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[class*='ri-'] {
margin-left: 3px !important;
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { translate } from '@/i18n'
import { handleMatched } from '@/utils/routes'
const route = useRoute()
const routesStore = useRoutesStore()
const { getRoutes: routes } = storeToRefs(routesStore)
const breadcrumbList = computed(() =>
handleMatched(routes.value, route.path).filter(
(item: any) => !item.meta.breadcrumbHidden
)
)
const handleTo = (path: string | undefined = '') => {
return { path }
}
</script>
<template>
<el-breadcrumb class="vab-breadcrumb" separator=">">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="index"
:to="handleTo(item.redirect)"
>
<vab-icon
v-if="item.meta.icon"
:icon="item.meta.icon"
:is-custom-svg="item.meta.isCustomSvg"
/>
<span v-if="item.meta.title">{{ translate(item.meta.title) }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<style lang="scss" scoped>
.vab-breadcrumb {
height: $base-nav-height;
font-size: $base-font-size-default;
line-height: $base-nav-height;
:deep() {
.el-breadcrumb__item {
.el-breadcrumb__inner {
font-weight: normal;
color: #515a6e;
i,
svg {
margin-right: 3px;
}
}
&:last-child {
.el-breadcrumb__inner {
a {
color: #999;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
defineProps({
header: {
type: String,
default: '',
},
bodyStyle: {
type: Object,
default: () => {
return {}
},
},
shadow: {
type: String,
default: 'never',
} as any,
skeleton: {
type: Boolean,
default: false,
},
skeletonRows: {
type: Number,
default: 4, //显示的数量会比传入的数量多 1
},
})
let timer: any = null
const skeletonShow = ref(true)
timer = setTimeout(() => {
skeletonShow.value = false
}, 500)
onBeforeRouteLeave((to, from, next) => {
clearInterval(timer)
next()
})
</script>
<template>
<el-card :body-style="bodyStyle" class="vab-card" :shadow="shadow">
<template v-if="$slots.header || header" #header>
<slot name="header">{{ header }}</slot>
</template>
<el-skeleton
v-if="skeleton"
animated
:loading="skeletonShow"
:rows="skeletonRows"
>
<template #default>
<slot class="vab-card-transition" />
</template>
</el-skeleton>
<slot v-else class="vab-card-transition" />
</el-card>
</template>
<style lang="scss" scoped>
.vab-card {
&-transition {
transition: $base-transition;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
defineProps({
shadow: {
type: String,
default: 'never',
} as any,
colorFrom: {
type: String,
default: '',
},
colorTo: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
})
</script>
<template>
<el-card
class="vab-colorful-card"
:shadow="shadow"
:style="{
background: `linear-gradient(50deg, ${colorFrom}, ${colorTo})`,
}"
>
<template #header>{{ title }}</template>
<vab-icon v-if="icon" :icon="icon" />
<slot />
</el-card>
</template>
<style lang="scss" scoped>
.vab-colorful-card {
position: relative;
min-height: 120px;
cursor: pointer;
* {
color: var(--el-color-white);
}
:deep() {
.el-card__header {
color: var(--el-color-white);
border-bottom: 0;
}
.el-card__body {
padding-top: 0;
}
}
i {
position: absolute;
top: -30px;
right: 20px;
font-size: 60px;
transform: rotate(15deg);
}
}
</style>

View File

@@ -0,0 +1,392 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { isExternal } from '@/utils/validate'
import { translate } from '@/i18n'
import { useRoutesStore } from '@/store/modules/routes'
import { defaultOpeneds, openFirstMenu, uniqueOpened } from '@/config'
import { useSettingsStore } from '@/store/modules/settings'
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const { theme, collapse } = storeToRefs(settingsStore)
const { foldSideBar, openSideBar } = settingsStore
const routesStore = useRoutesStore()
const {
getTab: tab,
getTabMenu: tabMenu,
getActiveMenu: activeMenu,
getRoutes: routes,
getPartialRoutes: partialRoutes,
}: any = storeToRefs(routesStore)
const handleTabClick = () => {
nextTick(() => {
if (isExternal(tabMenu.value.path)) {
window.open(tabMenu.value.path)
setTimeout(() => {
router.push(`/direct?path=/`)
}, 500)
} else if (openFirstMenu)
router.push(tabMenu.value.redirect || tabMenu.value)
})
}
watchEffect(() => {
const foldUnfold: any = document.querySelector(
'.fold-unfold'
) as HTMLElement
if (theme.value.layout === 'column' && route.meta.noColumn) {
foldSideBar()
if (foldUnfold) foldUnfold.style = 'display:none'
} else {
openSideBar()
if (foldUnfold) foldUnfold.style = ''
}
})
</script>
<template>
<el-scrollbar
class="vab-column-bar-container"
:class="{
'is-collapse': collapse,
['vab-column-bar-container-' + theme.columnStyle]: true,
}"
>
<vab-logo />
<el-tabs
v-model="tab.data"
tab-position="left"
@tab-click="handleTabClick"
>
<template v-for="(item, index) in routes" :key="index + item.name">
<el-tab-pane :name="item.name">
<template #label>
<div
class="vab-column-grid"
:class="{
['vab-column-grid-' + theme.columnStyle]: true,
}"
:title="translate(item.meta.title)"
>
<div>
<vab-icon
v-if="item.meta.icon"
:icon="item.meta.icon"
:is-custom-svg="item.meta.isCustomSvg"
/>
<span>
{{ translate(item.meta.title) }}
</span>
</div>
</div>
</template>
</el-tab-pane>
</template>
</el-tabs>
<el-menu
:background-color="variables['column-second-menu-background']"
:default-active="activeMenu.data"
:default-openeds="defaultOpeneds"
mode="vertical"
:unique-opened="uniqueOpened"
>
<el-divider>
{{ translate(tabMenu ? tabMenu.meta.title : tabMenu) }}
</el-divider>
<template v-for="item in partialRoutes" :key="item.path">
<vab-menu v-if="!item.meta.hidden" :item="item" />
</template>
</el-menu>
</el-scrollbar>
</template>
<style lang="scss" scoped>
@use 'sass:math';
@mixin active {
&:hover {
color: var(--el-color-primary);
background-color: $base-column-second-menu-active !important;
i,
svg {
color: var(--el-color-primary);
}
}
&.is-active {
color: var(--el-color-primary);
background-color: $base-column-second-menu-active !important;
}
}
.vab-column-bar-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: var(--el-left-menu-width);
height: 100vh;
overflow: hidden;
background: $base-column-second-menu-background;
box-shadow: $base-box-shadow;
&-vertical,
&-card,
&-arrow {
:deep() {
.el-tabs + .el-menu {
left: $base-left-menu-width-min;
width: calc(
var(--el-left-menu-width) - #{$base-left-menu-width-min}
);
border: 0;
}
}
}
&-horizontal {
:deep() {
.logo-container-column {
.logo {
width: $base-left-menu-width-min * 1.3 !important;
}
.title {
margin-left: $base-left-menu-width-min * 1.3 !important;
}
}
.el-tabs + .el-menu {
left: $base-left-menu-width-min * 1.3;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} * 1.3
);
border: 0;
}
}
}
&-card {
:deep() {
.el-tabs {
.el-tabs__item {
padding: 5px !important;
.vab-column-grid {
width: $base-left-menu-width-min - 10 !important;
height: $base-left-menu-width-min - 10 !important;
border-radius: 5px;
}
&.is-active {
background: transparent !important;
.vab-column-grid {
background: var(--el-color-primary);
}
}
}
}
.el-tabs + .el-menu {
left: $base-left-menu-width-min + 10;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} - 20px
);
}
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
min-width: 180px;
margin-bottom: 5px;
border-radius: 5px;
}
}
}
&-arrow {
:deep() {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
.vab-column-grid {
background: transparent !important;
&:after {
position: absolute;
right: 0;
width: 0;
height: 0;
overflow: hidden;
content: '';
border-color: transparent
#{var(--el-color-white)} transparent
transparent;
border-style: solid dashed dashed;
border-width: 8px;
}
}
}
}
}
.el-tabs + .el-menu {
left: $base-left-menu-width-min + 10;
width: calc(
var(--el-left-menu-width) -
#{$base-left-menu-width-min} - 20px
);
}
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
min-width: 180px;
margin-bottom: 5px;
border-radius: 5px;
}
}
}
.vab-column-grid {
display: flex;
align-items: center;
width: $base-left-menu-width-min;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
word-break: break-all;
white-space: nowrap;
&-vertical,
&-card,
&-arrow {
justify-content: center;
height: $base-left-menu-width-min;
> div {
svg {
position: relative;
display: block;
width: $base-font-size-default + 2;
height: $base-font-size-default + 2;
margin: auto !important;
}
[class*='ri-'] {
display: block;
height: 20px;
}
}
}
&-horizontal {
justify-content: left;
width: $base-left-menu-width-min * 1.3;
height: #{math.div($base-left-menu-width-min, 1.3)};
padding-left: #{math.div($base-padding, 2)};
}
}
:deep() {
* {
transition: $base-transition;
}
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-tabs {
position: fixed;
box-shadow: $base-box-shadow;
.el-tabs__header.is-left {
margin-right: 0 !important;
.el-tabs__nav-wrap.is-left {
margin-right: 0 !important;
background: $base-column-first-menu-background;
.el-tabs__nav-scroll {
height: calc(
100vh - #{$base-logo-height}
) !important;
overflow-y: auto !important;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.el-tabs__nav {
height: auto !important;
overflow: hidden !important;
background: $base-column-first-menu-background;
}
}
}
.el-tabs__item {
height: auto;
padding: 0;
color: var(--el-color-white);
&.is-active {
background: var(--el-color-primary);
}
}
}
.el-tabs__nav-prev,
.el-tabs__active-bar.is-left,
.el-tabs--left .el-tabs__nav-wrap.is-left::after {
display: none;
}
.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable {
padding: 0 !important;
}
.el-menu {
border: 0;
.el-divider {
margin: 0 0 #{$base-margin} 0;
background-color: #f6f6f6;
&__text {
color: var(--el-color-black);
}
}
.el-menu-item,
.el-sub-menu__title {
height: $base-menu-item-height;
overflow: hidden;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include active;
}
}
}
&.is-collapse {
:deep() {
width: 0;
}
}
}
</style>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import { useErrorLogStore } from '@/store/modules/errorLog'
const errorLogStore = useErrorLogStore()
const { errorLogs } = storeToRefs(errorLogStore)
const { clearErrorLog } = errorLogStore
const state = reactive({
dialogVisible: false,
})
const searchList = [
{
title: '百度搜索',
url: 'https://www.baidu.com/baidu?wd=',
icon: 'baidu-line',
},
{
title: '谷歌搜索',
url: 'https://www.google.com/search?q=',
icon: 'google-line',
},
]
const clearAll = () => {
state.dialogVisible = false
clearErrorLog()
}
</script>
<template>
<div v-if="errorLogs.length > 0">
<el-badge
type="danger"
:value="errorLogs.length"
@click="state.dialogVisible = true"
>
<vab-icon icon="bug-line" />
</el-badge>
<el-dialog
v-model="state.dialogVisible"
append-to-body
title="admin-plus 异常捕获(温馨提示:错误必须解决)"
width="70%"
>
<el-table border :data="errorLogs">
<el-table-column label="报错路由">
<template #default="{ row }">
<a :href="row.url" target="_blank">
<el-tag type="success">{{ row.url }}</el-tag>
</a>
</template>
</el-table-column>
<el-table-column label="错误信息">
<template #default="{ row }">
<el-tag type="danger">{{ row.err.message }}</el-tag>
</template>
</el-table-column>
<el-table-column label="错误详情" width="120">
<template #default="{ row }">
<el-popover placement="top-start" trigger="hover">
{{ row.err.stack }}
<template #reference>
<el-button>查看</el-button>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template #default="{ row }">
<a
v-for="(item, index) in searchList"
:key="index"
:href="item.url + row.err.message"
target="_blank"
>
<el-button>
<vab-icon :icon="item.icon" />
{{ item.title }}
</el-button>
</a>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="state.dialogVisible = false">
</el-button>
<el-button type="danger" @click="clearAll">暂不显示</el-button>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-badge) {
.el-button {
display: flex;
align-items: center;
justify-items: center;
height: 28px;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const { toggleCollapse } = settingsStore
</script>
<template>
<vab-icon
class="fold-unfold"
:icon="collapse ? 'menu-unfold-line' : 'menu-fold-line'"
@click="toggleCollapse"
/>
</template>
<style lang="scss" scoped>
.fold-unfold {
color: var(--el-color-grey);
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const fullYear = new Date().getFullYear()
const settingsStore = useSettingsStore()
const { title } = storeToRefs(settingsStore)
// 国家法律法规要求显示备案号 实际项目请自行修改为自己的备案信息及域名
// const beianShow = ref(false)
</script>
<template>
<footer class="vab-footer">
Copyright
<vab-icon icon="copyright-line" />
{{ fullYear }} {{ title }}
<!-- <a
v-if="beianShow"
class="beian"
href="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
>
鲁ICP备2021002317号-1
</a> -->
</footer>
</template>
<style lang="scss" scoped>
.vab-footer {
display: flex;
align-items: center;
justify-content: center;
min-height: 55px;
padding: 0 #{$base-padding} 0 #{$base-padding};
color: rgba(0, 0, 0, 0.45);
background: var(--el-color-white);
border-top: 1px dashed #{$base-border-color};
i {
margin: 0 5px;
}
.beian {
margin-left: 5px;
user-select: none;
}
}
</style>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const { isFullscreen, toggle } = useFullscreen()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
</script>
<template>
<vab-icon
v-if="theme.showFullScreen"
:icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'"
@click="toggle"
/>
</template>

View File

@@ -0,0 +1,233 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { useRoutesStore } from '@/store/modules/routes'
defineProps({
layout: {
type: String,
default: 'horizontal',
},
})
const routesStore = useRoutesStore()
const { getActiveMenu: activeMenu, getRoutes: routes } =
storeToRefs(routesStore)
</script>
<template>
<div class="vab-header">
<div class="vab-main">
<el-row :gutter="20">
<el-col :span="6">
<vab-logo />
</el-col>
<el-col :span="18">
<div class="right-panel">
<el-menu
v-if="'horizontal' === layout"
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:default-active="activeMenu.data"
menu-trigger="hover"
mode="horizontal"
style="width: 100%"
:text-color="variables['menu-color']"
>
<template
v-for="(item, index) in routes.flatMap(
(route) =>
route['meta'] &&
route['meta']['levelHidden'] &&
route['children']
? [...route['children']]
: route
)"
>
<vab-menu
v-if="
item['meta'] && !item['meta']['hidden']
"
:key="index + item['name']"
:item="item"
:layout="layout"
/>
</template>
</el-menu>
<vab-error-log />
<vab-lock />
<vab-search />
<vab-notice />
<vab-full-screen />
<vab-language />
<vab-theme />
<vab-refresh />
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<style lang="scss" scoped>
@use 'sass:math';
$base-menu-height: 40px;
.vab-header {
display: flex;
align-items: center;
justify-items: flex-end;
height: $base-header-height;
background: $base-menu-background;
.vab-main {
padding: 0 #{$base-padding} 0 #{$base-padding};
.right-panel {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-header-height;
:deep() {
.el-sub-menu__icon-more {
margin-top: #{math.div($base-menu-height - 20, 2)} !important;
margin-right: 20px !important;
}
> .el-menu--horizontal.el-menu {
> .el-sub-menu > .el-sub-menu__title {
padding-right: 0;
> .el-sub-menu__icon-arrow {
position: relative !important;
margin-top: -5px !important;
margin-right: 0;
margin-left: 30px;
}
}
> .el-menu-item {
.el-tag {
position: relative !important;
margin-top: 0 !important;
margin-right: -20px;
margin-left: 25px;
}
.vab-dot {
float: right;
margin-top: #{math.div(
$base-header-height - 6,
2
)} +
1;
}
@media only screen and (max-width: 1199px) {
.el-tag {
display: none;
}
}
}
}
.el-menu {
border: 0 !important;
* {
border: 0 !important;
}
&.el-menu--horizontal {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: $base-menu-height;
border: 0 !important;
> .el-menu-item,
> .el-sub-menu {
height: $base-menu-height;
margin-right: 3px;
line-height: $base-menu-height;
border-radius: 3px;
.el-sub-menu__icon-arrow {
float: right;
margin-top: -6px;
}
> .el-sub-menu__title {
display: flex;
align-items: flex-start;
height: $base-menu-height;
line-height: $base-menu-height;
border: 0 !important;
border-radius: 3px;
}
}
}
[class*='ri-'],
.vab-icon {
margin-left: 0;
color: var(--el-color-white);
cursor: pointer;
}
.el-sub-menu {
.vab-icon {
margin-top: 12px !important;
}
}
.el-sub-menu,
.el-menu-item {
i,
.vab-icon {
color: inherit;
}
&.is-active {
border: 0 !important;
.el-sub-menu__title {
border: 0 !important;
}
}
}
.el-menu-item {
&.is-active {
background: var(--el-color-primary) !important;
}
}
}
.user-name {
color: var(--el-color-white);
}
.user-name + i {
color: var(--el-color-white);
}
[class*='ri-'] {
margin-left: $base-margin;
color: var(--el-color-white);
cursor: pointer;
}
button {
svg {
margin-right: 0;
color: var(--el-color-white);
cursor: pointer;
fill: var(--el-color-white);
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import getPageTitle from '@/utils/pageTitle'
const { locale } = useI18n()
const route = useRoute()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const { changeLanguage } = settingsStore
const handleCommand = (language: string) => {
changeLanguage(language)
locale.value = language
document.title = getPageTitle(route.meta.title)
}
</script>
<template>
<el-dropdown v-if="theme.showLanguage" @command="handleCommand">
<vab-icon icon="translate" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh">中文简体</el-dropdown-item>
<el-dropdown-item command="en">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { isExternal } from '@/utils/validate'
const props = defineProps({
to: {
type: String,
required: true,
},
})
const type = computed(() => (isExternal(props.to) ? 'a' : 'router-link'))
const linkProps = () =>
isExternal(props.to)
? {
href: props.to,
target: '_blank',
rel: 'noopener',
}
: { to: props.to }
</script>
<template>
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>

View File

@@ -0,0 +1,225 @@
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
const vFocus: any = {
mounted(el: HTMLElement) {
el.querySelector('input')?.focus()
},
}
const userStore = useUserStore()
const { avatar } = storeToRefs(userStore)
const settingsStore = useSettingsStore()
const { theme, lock, title } = storeToRefs(settingsStore)
const { handleLock: _handleLock, handleUnLock: _handleUnLock } =
settingsStore
const background = ref(
`https://gcore.jsdelivr.net/gh/zxwk1998/image/vab-image-lock/${Math.round(Math.random() * 31)}.jpg`
)
const randomBackground = () => {
background.value = `https://gcore.jsdelivr.net/gh/zxwk1998/image/vab-image-lock/${Math.round(Math.random() * 31)}.jpg`
}
const validatePass = (rule: any, value: string, callback: any) => {
if (value === '' || value !== '123456') {
callback(new Error('请输入正确的密码'))
} else {
callback()
}
}
const formRef = ref()
const form = ref({
password: '123456',
})
const rules = {
password: [{ validator: validatePass, trigger: 'blur' }],
}
let lockIcon = true
const handleUnLock = () => {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
lockIcon = false
setTimeout(async () => {
await _handleUnLock()
lockIcon = true
await randomBackground()
const el = document.querySelector(
'.vab-side-bar'
) as HTMLElement
if (el) el.removeAttribute('style')
}, 500)
}
})
}
const handleLock = () => {
_handleLock()
const el = document.querySelector('.vab-side-bar') as HTMLElement
if (el) el.style.display = 'none'
}
</script>
<template>
<vab-icon v-if="theme.showLock" icon="lock-line" @click="handleLock" />
<transition v-if="theme.showLock" mode="out-in" name="fade-transform">
<div v-if="lock" class="vab-screen-lock">
<div
class="vab-screen-lock-background"
:style="{
background: `fixed url(${background}) center`,
backgroundSize: '100% 100%',
filter: 'blur(10px)',
}"
></div>
<div class="vab-screen-lock-content">
<div class="vab-screen-lock-content-title">
<el-avatar :size="180" :src="avatar" />
<vab-icon
:icon="lockIcon ? 'lock-line' : 'lock-unlock-line'"
/>
{{ title }} {{ translate('屏幕已锁定') }}
</div>
<div class="vab-screen-lock-content-form">
<el-form
ref="formRef"
:model="form"
:rules="rules"
@submit.prevent
>
<el-form-item label="" :label-width="0" prop="password">
<el-input
v-model="form.password"
v-focus
autocomplete="off"
placeholder="请输出密码123456"
type="password"
>
<template #suffix>
<el-button
native-type="submit"
type="primary"
@click="handleUnLock"
>
<vab-icon
:icon="
lockIcon
? 'lock-line'
: 'lock-unlock-line'
"
/>
{{ translate('解锁') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</div>
<span @click="randomBackground">
{{ translate('切换壁纸') }}
</span>
</div>
</div>
</transition>
</template>
<style lang="scss" scoped>
.vab-screen-lock {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
font-weight: bold;
background-color: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
transition: $base-transition;
&-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index - 1;
}
&-content {
z-index: $base-z-index;
padding: 40px 95px 40px 95px;
color: #252a30;
text-align: center;
background: rgba(255, 255, 255, 0.6);
border-radius: 15px;
backdrop-filter: blur(10px);
> span {
font-size: $base-font-size-small;
cursor: pointer;
}
&-title {
line-height: 50px;
color: #252a30;
text-align: center;
.ri-lock-line,
.ri-lock-unlock-line {
display: block;
margin: auto !important;
font-size: 30px;
color: #252a30 !important;
transition: $base-transition;
}
}
&-form {
:deep() {
.el-input__inner {
width: 280px;
height: 40px;
line-height: 40px;
}
.el-input__wrapper {
padding-right: 0;
.el-input__suffix {
right: 0;
.el-button {
height: 40px;
line-height: 40px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
i {
margin-left: 0 !important;
}
}
.el-input__validateIcon {
display: none;
}
}
}
}
}
}
@media (max-width: 576px) {
.vab-screen-lock-content {
width: auto !important;
margin: 5vw;
}
}
}
</style>

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const { theme, logo, title } = storeToRefs(settingsStore)
</script>
<template>
<div
class="logo-container"
:class="{
['logo-container-' + theme.layout]: true,
}"
>
<router-link to="/">
<span class="logo">
<!-- 使用自定义svg示例 -->
<vab-icon v-if="logo" :icon="logo" is-custom-svg />
</span>
<span
class="title"
:class="{ 'hidden-xs-only': theme.layout === 'horizontal' }"
>
{{ title }}
</span>
</router-link>
</div>
</template>
<style lang="scss" scoped>
@mixin container {
position: relative;
height: $base-header-height;
overflow: hidden;
line-height: $base-header-height;
background: transparent;
}
@mixin logo {
display: inline-block;
width: 32px;
height: 32px;
vertical-align: middle;
color: $base-title-color;
fill: currentColor;
}
@mixin title {
display: inline-block;
margin-left: 5px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
line-height: 55px;
vertical-align: middle;
color: $base-title-color;
white-space: nowrap;
}
.logo-container {
&-horizontal,
&-common {
@include container;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
}
}
&-vertical,
&-column,
&-comprehensive,
&-float {
@include container;
height: $base-logo-height;
line-height: $base-logo-height;
text-align: center;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
max-width: calc(var(--el-left-menu-width) - 60px);
}
}
&-column {
background: $base-column-second-menu-background !important;
.logo {
position: fixed;
top: 0;
display: block;
width: $base-left-menu-width-min;
height: $base-logo-height;
margin: 0;
background: $base-column-first-menu-background;
}
.title {
padding-right: 15px;
padding-left: 15px;
margin-left: $base-left-menu-width-min !important;
color: var(--el-color-black) !important;
background: $base-column-second-menu-background !important;
@include title;
}
}
}
</style>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { isExternal } from '@/utils/validate'
import { translate } from '@/i18n'
import { isHashRouterMode } from '@/config'
const props = defineProps({
itemOrMenu: {
type: Object,
default() {
return null
},
},
})
const route = useRoute()
const router = useRouter()
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { device } = storeToRefs(settingsStore)
const { foldSideBar } = settingsStore
const handleLink = () => {
const routePath = props.itemOrMenu.path
const target = props.itemOrMenu.meta.target
if (target === '_blank') {
if (isExternal(routePath)) window.open(routePath)
else if (route.path !== routePath)
isHashRouterMode
? window.open(`/#${routePath}`)
: window.open(routePath)
} else {
if (isExternal(routePath)) window.location.href = routePath
else if (route.path !== routePath) {
if (device.value === 'mobile') foldSideBar()
router.push(props.itemOrMenu.path)
} else $pub('reload-router-view')
}
}
</script>
<template>
<el-menu-item :index="itemOrMenu.path" @click="handleLink">
<vab-icon
v-if="itemOrMenu.meta.icon"
:icon="itemOrMenu.meta.icon"
:is-custom-svg="itemOrMenu.meta.isCustomSvg"
:title="translate(itemOrMenu.meta.title)"
/>
<span :title="translate(itemOrMenu.meta.title)">
{{ translate(itemOrMenu.meta.title) }}
</span>
<el-tag v-if="itemOrMenu.meta.badge" effect="dark" type="danger">
{{ itemOrMenu.meta.badge }}
</el-tag>
<span v-if="itemOrMenu.meta.dot" class="vab-dot vab-dot-error">
<span />
</span>
</el-menu-item>
</template>
<style lang="scss" scoped>
@use 'sass:math';
:deep(.el-tag) {
position: absolute;
right: 20px;
height: 16px;
padding-right: 4px;
padding-left: 4px;
// margin-top: #{math.div($base-menu-item-height - 16, 2)};
line-height: 16px;
border: 0;
}
.vab-dot {
position: absolute !important;
right: 20px;
// margin-top: #{math.div($base-menu-item-height - 6, 2)};
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { translate } from '@/i18n'
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
itemOrMenu: {
type: Object,
default() {
return null
},
},
})
const settingsStore: any = useSettingsStore()
const { theme }: any = storeToRefs(settingsStore)
</script>
<template>
<el-sub-menu
:index="itemOrMenu.path"
:teleported="theme.layout != 'horizontal'"
>
<template #title>
<vab-icon
v-if="itemOrMenu.meta.icon"
:icon="itemOrMenu.meta.icon"
:is-custom-svg="itemOrMenu.meta.isCustomSvg"
:title="translate(itemOrMenu.meta.title)"
/>
<span :title="translate(itemOrMenu.meta.title)">
{{ translate(itemOrMenu.meta.title) }}
</span>
</template>
<slot />
</el-sub-menu>
</template>

View File

@@ -0,0 +1,83 @@
<template>
<component
:is="menuComponent"
v-if="!item.meta.hidden"
:item-or-menu="item"
>
<template v-if="item.children && item.children.length">
<vab-menu
v-for="route in item.children"
:key="route.path"
:item="route"
/>
</template>
</component>
</template>
<script lang="ts">
/* 防止偶发性自动导入失败 */
import { computed, defineComponent } from 'vue'
import { storeToRefs } from 'pinia'
import { useSettingsStore } from '@/store/modules/settings'
const imports = require.context('./components', true, /\.vue$/)
const Components: {
[key: string]: any
} = {}
imports.keys().forEach((key) => {
Components[key.replace(/(\/|\.|vue)/g, '')] = imports(key).default
})
export default defineComponent({
name: 'VabMenu',
components: Components,
props: {
item: {
type: Object,
required: true,
},
layout: {
type: String,
default: '',
},
},
setup(props) {
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const menuComponent = computed(() =>
props.item.children &&
props.item.children.some((_route: any) => {
return _route.meta.hidden !== true
})
? 'VabSubMenu'
: 'VabMenuItem'
)
return {
collapse,
menuComponent,
}
},
})
</script>
<style lang="scss" scoped>
.vab-menu-children-height {
height: 60vh !important;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
</style>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss">
.el-popper.is-light {
border: 0 !important;
}
</style>

View File

@@ -0,0 +1,165 @@
<script lang="ts" setup>
import { useRoutesStore } from '@/store/modules/routes'
import { translate } from '@/i18n'
import { isExternal } from '@/utils/validate'
import { openFirstMenu } from '@/config'
defineProps({
layout: {
type: String,
default: '',
},
})
const router = useRouter()
const routesStore: any = useRoutesStore()
const {
getTab: tab,
getTabMenu: tabMenu,
getRoutes: routes,
} = storeToRefs(routesStore)
const handleTabClick = () => {
nextTick(() => {
if (isExternal(tabMenu.value.path)) {
window.open(tabMenu.value.path)
setTimeout(() => {
router.push('/')
}, 1000)
} else if (openFirstMenu)
router.push(tabMenu.value.redirect || tabMenu.value)
})
}
</script>
<template>
<div class="vab-nav">
<el-row :gutter="15">
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="4">
<div class="left-panel">
<vab-fold v-if="layout !== 'float'" />
<el-tabs
v-if="layout === 'comprehensive'"
v-model="tab.data"
tab-position="top"
@tab-click="handleTabClick"
>
<template
v-for="(item, index) in routes"
:key="index + item.name"
>
<el-tab-pane :name="item.name">
<template #label>
<vab-icon
v-if="item.meta.icon"
:icon="item.meta.icon"
:is-custom-svg="item.meta.isCustomSvg"
style="min-width: 16px"
/>
<span>
{{ translate(item.meta.title) }}
</span>
</template>
</el-tab-pane>
</template>
</el-tabs>
<vab-breadcrumb v-else class="hidden-xs-only" />
</div>
</el-col>
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="20">
<div class="right-panel">
<vab-error-log />
<vab-lock />
<vab-search />
<vab-notice />
<vab-full-screen />
<vab-language />
<vab-theme />
<vab-refresh />
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
.vab-nav {
position: relative;
height: $base-nav-height;
padding-right: $base-padding;
padding-left: $base-padding;
overflow: hidden;
user-select: none;
background: var(--el-color-white);
box-shadow: $base-box-shadow;
.left-panel {
display: flex;
align-items: center;
justify-items: center;
height: $base-nav-height;
:deep() {
.fold-unfold {
margin-right: $base-margin;
}
.el-tabs {
width: 100%;
margin-left: $base-margin;
.el-tabs__header {
margin: 0;
> .el-tabs__nav-wrap {
display: flex;
align-items: center;
.el-icon-arrow-left,
.el-icon-arrow-right {
font-weight: 600;
color: var(--el-color-grey);
}
.el-tabs__item {
i {
margin-right: 3px;
}
}
}
}
}
.el-tabs__nav-wrap::after {
display: none;
}
}
}
.right-panel {
display: flex;
align-content: center;
align-items: center;
justify-content: flex-end;
height: $base-nav-height;
:deep() {
[class*='ri-'] {
margin-left: $base-margin;
color: var(--el-color-grey);
cursor: pointer;
}
button {
[class*='ri-'] {
margin-left: 0;
color: var(--el-color-white);
cursor: pointer;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,356 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
import { getList } from '@/api/notice'
const $baseMessage: any = inject('$baseMessage')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const activeName = ref('notice')
const notices: any = ref([])
const badge = ref(undefined)
const fetchData = async () => {
const {
data: { list, total },
} = await getList()
notices.value = list
badge.value = total === 0 ? undefined : total
}
nextTick(() => {
if (theme.value.showNotice) fetchData()
})
const handleClick = () => {
fetchData()
}
const handleClearNotice = () => {
badge.value = undefined
notices.value = []
$baseMessage('清空消息成功', 'success', 'vab-hey-message-success')
}
const formatTime = (time: string) => {
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return date.toLocaleDateString()
}
</script>
<template>
<el-badge
v-if="theme.showNotice"
class="notice-badge"
type="danger"
:value="badge"
>
<el-popover
placement="bottom-end"
popper-class="notice-popover"
:show-arrow="false"
:width="380"
>
<template #reference>
<div class="notice-trigger">
<vab-icon icon="notification-line" />
</div>
</template>
<div class="notice-container">
<div class="notice-header">
<h3>{{ translate('消息中心') }}</h3>
<el-button
class="clear-btn"
size="small"
text
type="primary"
@click="handleClearNotice"
>
<vab-icon icon="delete-bin-line" />
{{ translate('清空') }}
</el-button>
</div>
<el-tabs
v-model="activeName"
class="notice-tabs"
@tab-click="handleClick"
>
<el-tab-pane :label="translate('通知')" name="notice">
<div class="notice-list">
<el-scrollbar height="320px">
<div
v-if="notices.length === 0"
class="empty-state"
>
<vab-icon
class="empty-icon"
icon="notification-off-line"
/>
<p>{{ translate('暂无通知') }}</p>
</div>
<div v-else class="notice-items">
<div
v-for="(item, index) in notices"
:key="index"
class="notice-item"
>
<div class="notice-avatar">
<el-avatar
:size="40"
:src="item.image"
/>
<div
v-if="!item.read"
class="notice-status"
></div>
</div>
<div class="notice-content">
<div
class="notice-text"
v-html="item.notice"
></div>
<div class="notice-time">
{{
formatTime(
item.time || new Date()
)
}}
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</el-tab-pane>
<el-tab-pane :label="translate('邮件')" name="email">
<div class="notice-list">
<el-scrollbar height="320px">
<div
v-if="notices.length === 0"
class="empty-state"
>
<vab-icon
class="empty-icon"
icon="mail-line"
/>
<p>{{ translate('暂无邮件') }}</p>
</div>
<div v-else class="notice-items">
<div
v-for="(item, index) in notices"
:key="index"
class="notice-item"
>
<div class="notice-avatar">
<el-avatar
:size="40"
:src="item.image"
/>
<div
v-if="!item.read"
class="notice-status"
></div>
</div>
<div class="notice-content">
<div class="notice-text">
{{ item.email }}
</div>
<div class="notice-time">
{{
formatTime(
item.time || new Date()
)
}}
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-popover>
</el-badge>
</template>
<style lang="scss" scoped>
.notice-badge {
:deep(.el-badge__content) {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
}
}
.notice-trigger {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.notice-container {
.notice-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.clear-btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
&:hover {
color: var(--el-color-danger);
background: var(--el-color-danger-light-9);
}
}
}
.notice-tabs {
:deep(.el-tabs__header) {
padding: 0 20px;
margin: 0;
.el-tabs__nav-wrap {
&::after {
display: none;
}
}
.el-tabs__item {
padding: 12px 16px;
font-size: 14px;
transition: all 0.3s ease;
&.is-active {
font-weight: 500;
color: var(--el-color-primary);
}
&:hover {
color: var(--el-color-primary);
}
}
.el-tabs__active-bar {
min-width: 28px;
}
}
:deep(.el-tabs__content) {
padding: 0;
}
}
.notice-list {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--el-text-color-placeholder);
.empty-icon {
margin-bottom: 12px;
font-size: 48px;
opacity: 0.6;
}
p {
margin: 0;
font-size: 14px;
}
}
.notice-items {
padding: 8px 0;
.notice-item {
display: flex;
align-items: flex-start;
padding: 12px 20px;
margin: 0 8px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
background: var(--el-fill-color-light);
}
.notice-avatar {
position: relative;
flex-shrink: 0;
margin-right: 12px;
.notice-status {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: var(--el-color-danger);
border: 2px solid #fff;
border-radius: 50%;
}
}
.notice-content {
flex: 1;
min-width: 0;
.notice-text {
margin-bottom: 4px;
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-primary);
word-break: break-all;
}
.notice-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
}
}
</style>
<style lang="scss">
.notice-popover {
padding: 0 !important;
border: 1px solid var(--el-border-color-lighter) !important;
border-radius: 12px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
.el-popover__title {
display: none;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<el-col :span="24">
<div class="bottom-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps({
span: {
type: Number,
default: 14,
},
})
</script>
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="left-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps({
span: {
type: Number,
default: 10,
},
})
</script>
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="right-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<el-col :span="24">
<div class="top-panel">
<slot />
</div>
</el-col>
</template>

View File

@@ -0,0 +1,60 @@
<template>
<el-row class="vab-query-form" :gutter="0">
<slot />
</el-row>
</template>
<style lang="scss" scoped>
@use 'sass:math';
@mixin panel {
display: flex;
flex-wrap: wrap;
align-content: center;
align-items: center;
justify-content: flex-start;
min-height: $base-input-height;
margin: 0 0 #{math.div($base-margin, 2)} 0;
.el-form-item__content {
display: flex;
align-items: center;
}
> .el-button {
margin: 0 10px #{math.div($base-margin, 2)} 0 !important;
}
}
.vab-query-form {
:deep() {
.el-form-item:first-child {
margin: 0 0 #{math.div($base-margin, 2)} 0 !important;
}
.el-form-item + .el-form-item {
margin: 0 0 #{math.div($base-margin, 2)} 0 !important;
.el-button {
margin: 0 0 0 10px !important;
}
}
.top-panel {
@include panel;
}
.bottom-panel {
@include panel;
border-top: 1px solid #dcdfe6;
}
.left-panel {
@include panel;
}
.right-panel {
@include panel;
justify-content: flex-end;
}
}
}
</style>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const refreshRoute = () => {
$pub('reload-router-view')
}
</script>
<template>
<vab-icon
v-if="theme.showRefresh"
icon="refresh-line"
@click="refreshRoute"
/>
</template>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import VabProgress from 'nprogress'
import { useSettingsStore } from '@/store/modules/settings'
import { useTabsStore } from '@/store/modules/tabs'
import { handleActivePath } from '@/utils/routes'
import { keepAliveMaxNum } from '@/config'
import { VabRouteRecord } from '/#/router'
const route = useRoute()
const $sub: any = inject('$sub')
const $unsub: any = inject('$unsub')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const tabsStore = useTabsStore()
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
const componentRef = ref()
const routerKey = ref()
const keepAliveNameList = ref()
const updateKeepAliveNameList = (refreshRouteName = null) => {
keepAliveNameList.value = visitedRoutes.value
.filter(
(item: VabRouteRecord) =>
!item.meta.noKeepAlive && item.name !== refreshRouteName
)
.flatMap((item: VabRouteRecord) => item.name)
}
// 更新KeepAlive缓存页面
watchEffect(() => {
routerKey.value = handleActivePath(route, true)
updateKeepAliveNameList()
})
// 获取源码地址
$sub('get-code', () => {
window.open(
`https://github.com/zxwk2024/admin-plus/blob/main/${componentRef.value.$options.__source}`
)
})
$sub('reload-router-view', (refreshRouteName: any = route.name) => {
if (theme.value.showProgressBar) VabProgress.start()
const cacheActivePath = routerKey.value
routerKey.value = null
updateKeepAliveNameList(refreshRouteName)
nextTick(() => {
routerKey.value = cacheActivePath
updateKeepAliveNameList()
})
setTimeout(() => {
if (theme.value.showProgressBar) VabProgress.done()
}, 200)
})
// onUnmounted(() => {
// $unsub('get-code')
// $unsub('reload-router-view')
// })
</script>
<template>
<router-view v-slot="{ Component }">
<transition
mode="out-in"
:name="theme.showPageTransition ? 'fade-transform' : 'no-transform'"
>
<keep-alive :include="keepAliveNameList" :max="keepAliveMaxNum">
<component
:is="Component"
:key="routerKey"
ref="componentRef"
/>
</keep-alive>
</transition>
</router-view>
</template>

View File

@@ -0,0 +1,517 @@
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useRoutesStore } from '@/store/modules/routes'
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
const MOD_KEY = isMac ? 'metaKey' : 'ctrlKey'
const show = ref(false)
const search = ref('')
const activeIndex = ref(0)
const inputRef = ref<HTMLInputElement | null>(null)
const routesStore = useRoutesStore()
const { getRoutes } = storeToRefs(routesStore)
const router = useRouter()
// 递归平铺所有菜单项
function flattenMenus(
routes: any[],
basePath = '',
parentIcon = undefined
): any[] {
let result: any[] = []
routes.forEach((route) => {
if (route.meta && route.meta.hidden) return
const fullPath = route.path.startsWith('/')
? route.path
: `${basePath}/${route.path}`
const icon =
route.meta && route.meta.icon ? route.meta.icon : parentIcon
if (route.meta && route.meta.title) {
result.push({
title: route.meta.title,
path: fullPath,
icon,
...route.meta,
})
}
if (route.children && route.children.length > 0) {
result = result.concat(
flattenMenus(route.children, fullPath, icon)
)
}
})
return result
}
const menuList = computed(() => flattenMenus(getRoutes.value))
// 搜索历史localStorage
const HISTORY_KEY = 'vab_search_history'
const searchHistory = ref<string[]>([])
function loadHistory() {
const raw = localStorage.getItem(HISTORY_KEY)
searchHistory.value = raw ? JSON.parse(raw) : []
}
function saveHistory(keyword: string) {
if (!keyword) return
let arr = searchHistory.value.filter((item) => item !== keyword)
arr.unshift(keyword)
if (arr.length > 8) arr = arr.slice(0, 8)
searchHistory.value = arr
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}
function removeHistory(keyword: string) {
searchHistory.value = searchHistory.value.filter(
(item) => item !== keyword
)
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value))
}
function clearHistory() {
searchHistory.value = []
localStorage.removeItem(HISTORY_KEY)
}
// 简单模糊搜索(可扩展为拼音/多字段)
const filteredMenus = computed(() => {
if (!search.value) return menuList.value
return menuList.value.filter(
(item) =>
item.title.toLowerCase().includes(search.value.toLowerCase()) ||
(item.path &&
item.path
.toLowerCase()
.includes(search.value.toLowerCase()))
)
})
function openSearch() {
show.value = true
search.value = ''
activeIndex.value = 0
loadHistory()
nextTick(() => {
inputRef.value?.focus()
})
}
function closeSearch() {
show.value = false
}
function selectMenu(index: number) {
const item = filteredMenus.value[index]
if (item && item.path) {
saveHistory(search.value)
closeSearch()
// 检查是否为外链
if (
item.target === '_blank' ||
item.path.startsWith('http://') ||
item.path.startsWith('https://') ||
item.path.startsWith('//')
) {
// 外链使用window.open
window.open(item.path, '_blank')
} else {
// 内部路由使用router.push
router.push(item.path)
}
}
}
function selectHistory(keyword: string) {
search.value = keyword
activeIndex.value = 0
nextTick(() => {
inputRef.value?.focus()
})
}
function onKeydown(e: KeyboardEvent) {
if (show.value) {
if (e.key === 'Escape') {
closeSearch()
} else if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex.value =
(activeIndex.value + 1) % filteredMenus.value.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex.value =
(activeIndex.value - 1 + filteredMenus.value.length) %
filteredMenus.value.length
} else if (e.key === 'Enter') {
selectMenu(activeIndex.value)
}
} else {
if ((e as any)[MOD_KEY] && e.key.toLowerCase() === 'k') {
e.preventDefault()
openSearch()
}
}
}
onMounted(() => {
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeydown)
})
function onItemClick(idx: number) {
selectMenu(idx)
}
function onInputKeydown(e: KeyboardEvent) {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'Enter' ||
e.key === 'Escape'
) {
// 交由全局keydown处理
e.preventDefault()
}
}
</script>
<template>
<span class="vab-search-trigger hidden-xs-only">
<vab-icon
icon="search-line"
title="菜单搜索 ({{ isMac ? '⌘+K' : 'Ctrl+K' }})"
@click="openSearch"
/>
</span>
<teleport to="body">
<transition name="fade">
<div
v-if="show"
class="vab-spotlight-mask"
@click.self="closeSearch"
>
<div class="vab-spotlight-card">
<div class="vab-spotlight-input-wrap">
<vab-icon
class="vab-spotlight-input-icon"
icon="search-line"
/>
<input
ref="inputRef"
v-model="search"
autocomplete="off"
class="vab-spotlight-input"
:placeholder="
isMac ? '搜索菜单… (⌘+K)' : '搜索菜单… (Ctrl+K)'
"
@keydown="onInputKeydown"
/>
</div>
<div
v-if="searchHistory.length"
class="vab-spotlight-history-wrap"
>
<div class="vab-spotlight-history">
<div class="vab-spotlight-history-title">
搜索历史
<span
class="vab-spotlight-history-clear"
@click="clearHistory"
>
清空
</span>
</div>
<div class="vab-spotlight-history-list">
<span
v-for="item in searchHistory"
:key="item"
class="vab-spotlight-history-item"
>
<span @click="selectHistory(item)">
{{ item }}
</span>
<vab-icon
class="vab-spotlight-history-del"
icon="close-circle-fill"
@click.stop="removeHistory(item)"
/>
</span>
</div>
</div>
</div>
<ul class="vab-spotlight-list">
<li
v-for="(item, idx) in filteredMenus"
:key="item.path + item.title"
:class="[
'vab-spotlight-item',
{ active: idx === activeIndex },
]"
@mousedown.prevent="onItemClick(idx)"
@mouseenter="activeIndex = idx"
>
<vab-icon
v-if="item.icon"
class="vab-spotlight-item-icon"
:icon="item.icon"
/>
<div class="vab-spotlight-item-content">
<div class="vab-spotlight-item-title">
{{ item.title }}
</div>
<div class="vab-spotlight-item-path">
{{ item.path }}
</div>
</div>
</li>
<li
v-if="filteredMenus.length === 0"
class="vab-spotlight-empty"
>
<div v-if="!search">请输入关键词进行搜索</div>
<div v-else>无匹配菜单</div>
</li>
</ul>
</div>
</div>
</transition>
<button
class="vab-spotlight-fab"
title="菜单搜索 ({{ isMac ? '⌘+K' : 'Ctrl+K' }})"
@click="openSearch"
>
<vab-icon icon="search-line" />
</button>
</teleport>
</template>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.vab-spotlight-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(30, 34, 45, 0.25);
backdrop-filter: blur(2px);
}
.vab-spotlight-card {
display: flex;
flex-direction: column;
width: 600px;
max-width: 96vw;
padding: 0 0 8px 0;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 18px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18);
backdrop-filter: blur(12px) saturate(120%);
animation: fadeInUp 0.18s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: none;
}
}
.vab-spotlight-input-wrap {
display: flex;
align-items: center;
padding: 24px 28px 12px 28px;
}
.vab-spotlight-input-icon {
margin-right: 10px;
font-size: 22px;
color: #666;
}
.vab-spotlight-input {
flex: 1;
padding: 8px 0;
font-size: 20px;
color: #222;
outline: none;
background: transparent;
border: none;
}
.vab-spotlight-list {
max-height: 340px;
padding: 0 8px 0 8px;
margin: 0;
overflow-y: auto;
list-style: none;
}
.vab-spotlight-item {
display: flex;
align-items: center;
padding: 12px 18px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 10px;
transition: background 0.18s;
&:last-child {
margin-bottom: 0;
}
&.active,
&:hover {
background: #f5f5f5;
}
}
.vab-spotlight-item-icon {
margin-right: 14px;
font-size: 26px !important;
color: #666;
}
.vab-spotlight-item-content {
display: flex;
flex-direction: column;
min-width: 0;
}
.vab-spotlight-item-title {
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 600;
line-height: 1.2;
color: #222;
white-space: nowrap;
}
.vab-spotlight-item-path {
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
color: #8c8c8c;
white-space: nowrap;
}
.vab-spotlight-empty {
padding: 32px 0 24px 0;
font-size: 15px;
color: #aaa;
text-align: center;
}
.vab-spotlight-fab {
position: fixed;
right: 32px;
bottom: 32px;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
font-size: 28px;
color: #fff;
cursor: pointer;
outline: none;
background: #666;
border: none;
border-radius: 50%;
box-shadow: 0 4px 16px 0 rgba(102, 102, 102, 0.18);
transition:
background 0.18s,
box-shadow 0.18s,
transform 0.18s;
&:hover {
background: #888;
box-shadow: 0 8px 32px 0 rgba(102, 102, 102, 0.18);
transform: translateY(-2px) scale(1.06);
}
&:active {
background: #555;
transform: scale(0.98);
}
}
@media (max-width: 600px) {
.vab-spotlight-card {
width: 98vw;
min-width: 0;
padding: 0 0 8px 0;
}
.vab-spotlight-input-wrap {
padding: 18px 10px 10px 10px;
}
.vab-spotlight-fab {
right: 16px;
bottom: 16px;
width: 48px;
height: 48px;
font-size: 22px;
}
}
.vab-search-trigger {
display: inline-flex;
align-items: center;
font-size: 22px;
color: #666;
cursor: pointer;
transition: color 0.18s;
&:hover {
color: #666;
}
}
.vab-spotlight-history-wrap {
padding: 0 28px 0 28px;
margin-bottom: 8px;
}
.vab-spotlight-history-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
font-size: 13px;
color: #888;
}
.vab-spotlight-history-clear {
margin-left: 8px;
font-size: 12px;
color: #aaa;
cursor: pointer;
&:hover {
color: #ff4d4f;
}
}
.vab-spotlight-history-list {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-bottom: 8px;
}
.vab-spotlight-history-item {
display: inline-flex;
align-items: center;
padding: 2px 12px 2px 10px;
font-size: 14px;
color: #666;
cursor: pointer;
background: #f5f7fa;
border-radius: 16px;
transition:
background 0.18s,
color 0.18s;
&:hover {
color: #666;
background: #f5f5f5;
}
}
.vab-spotlight-history-del {
margin-left: 4px;
font-size: 14px;
color: #bbb;
cursor: pointer;
&:hover {
color: #ff4d4f;
}
}
</style>

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import variables from '@vab/styles/variables/variables.module.scss'
import { useRoutesStore } from '@/store/modules/routes'
import { useSettingsStore } from '@/store/modules/settings'
import { defaultOpeneds, uniqueOpened } from '@/config'
const props = defineProps({
layout: {
type: String,
default: 'vertical',
},
})
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const routesStore = useRoutesStore()
const {
getRoutes: routes,
getActiveMenu: activeMenu,
getPartialRoutes: partialRoutes,
} = storeToRefs(routesStore)
const handleRoutes = computed(() => {
return props.layout === 'comprehensive'
? partialRoutes.value
: routes.value.flatMap((route: any) =>
route.meta.levelHidden && route.children
? [...route.children]
: route
)
})
</script>
<template>
<el-scrollbar
class="vab-side-bar"
:class="{
'is-collapse': collapse,
'side-bar-common': layout === 'common',
}"
>
<vab-logo
v-if="
layout === 'vertical' ||
layout === 'comprehensive' ||
layout === 'float'
"
/>
<el-menu
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:collapse="collapse"
:collapse-transition="false"
:default-active="activeMenu.data"
:default-openeds="defaultOpeneds"
menu-trigger="click"
mode="vertical"
:text-color="variables['menu-color']"
:unique-opened="uniqueOpened"
>
<template
v-for="(item, index) in handleRoutes"
:key="index + item.name"
>
<vab-menu v-if="!item.meta.hidden" :item="item" />
</template>
</el-menu>
</el-scrollbar>
</template>
<style lang="scss" scoped>
@mixin active {
&:hover {
color: var(--el-color-white);
background-color: $base-menu-active;
}
&.is-active {
color: var(--el-color-white);
background-color: $base-menu-active;
}
}
.vab-side-bar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: $base-z-index + 1;
width: var(--el-left-menu-width);
height: 100vh;
overflow: hidden;
background: $base-menu-background;
box-shadow: $base-box-shadow;
transition: $base-transition;
&.side-bar-common {
top: $base-header-height;
height: calc(100vh - #{$base-header-height});
}
&.is-collapse {
width: $base-left-menu-width-min;
border-right: 0;
:deep() {
.el-menu--collapse.el-menu {
> .el-menu-item,
> .el-sub-menu {
text-align: center;
.el-tag {
display: none;
}
}
}
.el-menu-item,
.el-sub-menu {
text-align: left;
}
.el-menu--collapse {
border-right: 0;
.el-sub-menu__icon-arrow {
right: 10px;
margin-top: -3px;
}
}
}
}
:deep() {
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu-item,
.el-sub-menu__title {
height: $base-menu-item-height;
overflow: hidden;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
i {
color: inherit;
}
}
.el-menu-item {
@include active;
}
}
}
</style>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss">
@mixin menuActiveHover {
&:hover,
&.is-active {
i {
color: var(--el-color-white);
}
color: var(--el-color-white);
background: var(--el-color-primary);
.el-sub-menu__title {
i {
color: var(--el-color-white);
}
color: var(--el-color-white);
background: var(--el-color-primary);
}
}
}
.el-menu {
border-right: 0;
}
.el-popper {
.el-menu--vertical {
.el-menu-item,
.el-sub-menu {
height: $base-menu-item-height;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include menuActiveHover;
i {
color: inherit;
}
.el-sub-menu__title {
height: $base-menu-item-height;
text-overflow: ellipsis;
line-height: $base-menu-item-height;
vertical-align: middle;
white-space: nowrap;
@include menuActiveHover;
}
}
}
}
</style>

View File

@@ -0,0 +1,614 @@
<script lang="ts" setup>
import { useTabsStore } from '@/store/modules/tabs'
import { useRoutesStore } from '@/store/modules/routes'
import { useSettingsStore } from '@/store/modules/settings'
import { handleActivePath, handleTabs } from '@/utils/routes'
import { translate } from '@/i18n'
import { VabRoute, VabRouteRecord } from '/#/router'
defineProps({
layout: {
type: String,
default: '',
},
})
const $pub: any = inject('$pub')
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const routesStore = useRoutesStore()
const { getRoutes: routes } = storeToRefs(routesStore)
const tabsStore = useTabsStore()
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
const {
addVisitedRoute,
delVisitedRoute,
delOthersVisitedRoutes,
delLeftVisitedRoutes,
delRightVisitedRoutes,
delAllVisitedRoutes,
} = tabsStore
const tabActive = ref('')
const active = ref(false)
const hoverRoute = ref()
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const isActive = (path: string) => path === handleActivePath(route, true)
const isNoCLosable = (tag: any) => tag.meta.noClosable
const handleTabClick = (tab: any) => {
if (!isActive(tab.name)) router.push(visitedRoutes.value[tab.index])
}
const handleVisibleChange = (val: boolean) => {
active.value = val
}
const initNoCLosableTabs = (routes: VabRouteRecord[]) => {
routes.forEach((_route) => {
if (_route.meta.noClosable) addTabs(_route)
if (_route.children) initNoCLosableTabs(_route.children)
})
}
/**
* 添加标签页
* @param tag route
* @returns {Promise<void>}
*/
const addTabs = async (tag: VabRoute | VabRouteRecord) => {
const tab = handleTabs(tag)
if (tab) {
await addVisitedRoute(tab)
tabActive.value = tab.path
}
}
/**
* 根据原生路径删除标签中的标签
* @param rawPath 原生路径
* @returns {Promise<void>}
*/
const handleTabRemove: any = async (rawPath: string) => {
if (isActive(rawPath)) await toLastTab()
await delVisitedRoute(rawPath)
}
const handleCommand = (command: string) => {
switch (command) {
case 'refreshThisTab':
refreshThisTab()
break
case 'closeOthersTabs':
closeOthersTabs()
break
case 'closeLeftTabs':
closeLeftTabs()
break
case 'closeRightTabs':
closeRightTabs()
break
case 'closeAllTabs':
closeAllTabs()
break
}
}
/**
* 刷新当前标签页
* @returns {Promise<void>}
*/
const refreshThisTab = () => {
$pub('reload-router-view')
}
/**
* 删除其他标签页
* @returns {Promise<void>}
*/
const closeOthersTabs = async () => {
if (hoverRoute.value) {
await router.push(hoverRoute.value)
await delOthersVisitedRoutes(hoverRoute.value.path)
} else await delOthersVisitedRoutes(handleActivePath(route, true))
await closeMenu()
}
/**
* 删除左侧标签页
* @returns {Promise<void>}
*/
const closeLeftTabs = async () => {
if (hoverRoute.value) {
await router.push(hoverRoute.value)
await delLeftVisitedRoutes(hoverRoute.value.path)
} else await delLeftVisitedRoutes(handleActivePath(route, true))
await closeMenu()
}
/**
* 删除右侧标签页
* @returns {Promise<void>}
*/
const closeRightTabs = async () => {
if (hoverRoute.value) {
await router.push(hoverRoute.value)
await delRightVisitedRoutes(hoverRoute.value.path)
} else await delRightVisitedRoutes(handleActivePath(route, true))
await closeMenu()
}
/**
* 删除所有标签页
* @returns {Promise<void>}
*/
const closeAllTabs = async () => {
await delAllVisitedRoutes()
await toLastTab()
await closeMenu()
}
/**
* 跳转最后一个标签页
*/
const toLastTab = async () => {
const latestView = visitedRoutes.value
.filter((_: any) => _.path !== handleActivePath(route, true))
.slice(-1)[0]
if (latestView) await router.push(latestView)
else await router.push('/')
}
const { x, y } = useMouse()
const openMenu = () => {
left.value = x.value
top.value = y.value
visible.value = true
}
const closeMenu = () => {
visible.value = false
hoverRoute.value = null
}
initNoCLosableTabs(routes.value)
watch(
() => route.fullPath,
() => {
addTabs(route)
},
{
immediate: true,
}
)
watchEffect(() => {
if (visible.value) document.body.addEventListener('click', closeMenu)
else document.body.removeEventListener('click', closeMenu)
})
</script>
<template>
<div class="vab-tabs">
<vab-fold v-if="layout === 'common'" />
<el-tabs
v-model="tabActive"
class="vab-tabs-content"
:class="{
['vab-tabs-content-' + theme.tabsBarStyle]: true,
}"
type="card"
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
>
<el-tab-pane
v-for="item in visitedRoutes"
:key="item.path"
:closable="!isNoCLosable(item)"
:name="item.path"
>
<template #label>
<span
style="display: inline-block"
@contextmenu.prevent="openMenu"
>
<template v-if="theme.showTabsIcon">
<vab-icon
v-if="item.meta && item.meta.icon"
:icon="item.meta.icon"
:is-custom-svg="item.meta.isCustomSvg"
/>
<!-- 如果没有图标那么取第二级的图标 -->
<vab-icon v-else :icon="item.parentIcon" />
</template>
<span v-if="item.meta && item.meta.title">
{{ translate(item.meta.title) }}
</span>
</span>
</template>
</el-tab-pane>
</el-tabs>
<el-dropdown
placement="bottom-end"
popper-class="vab-tabs-more-dropdown"
@command="handleCommand"
@visible-change="handleVisibleChange"
>
<span
class="vab-tabs-more"
:class="{ 'vab-tabs-more-active': active }"
>
<span class="vab-tabs-more-icon">
<i class="box box-t"></i>
<i class="box box-b"></i>
</span>
</span>
<template #dropdown>
<el-dropdown-menu class="tabs-more">
<el-dropdown-item command="refreshThisTab">
<vab-icon icon="refresh-line" />
<span>
{{ translate('刷新') }}
</span>
</el-dropdown-item>
<el-dropdown-item command="closeOthersTabs">
<vab-icon icon="close-line" />
<span>
{{ translate('关闭其他') }}
</span>
</el-dropdown-item>
<el-dropdown-item command="closeLeftTabs">
<vab-icon icon="arrow-left-line" />
<span>
{{ translate('关闭左侧') }}
</span>
</el-dropdown-item>
<el-dropdown-item command="closeRightTabs">
<vab-icon icon="arrow-right-line" />
<span>
{{ translate('关闭右侧') }}
</span>
</el-dropdown-item>
<el-dropdown-item command="closeAllTabs">
<vab-icon icon="close-line" />
<span>
{{ translate('关闭全部') }}
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<ul
v-if="visible"
class="contextmenu el-dropdown-menu"
:style="{ left: left + 'px', top: top + 'px' }"
>
<li class="el-dropdown-menu__item" @click="refreshThisTab">
<vab-icon icon="refresh-line" />
<span>{{ translate('刷新') }}</span>
</li>
<li
class="el-dropdown-menu__item"
:class="{ 'is-disabled': visitedRoutes.length === 1 }"
@click="closeOthersTabs"
>
<vab-icon icon="close-line" />
<span>{{ translate('关闭其他') }}</span>
</li>
<li
class="el-dropdown-menu__item"
:class="{ 'is-disabled': !visitedRoutes.indexOf(hoverRoute) }"
@click="closeLeftTabs"
>
<vab-icon icon="arrow-left-line" />
<span>{{ translate('关闭左侧') }}</span>
</li>
<li
class="el-dropdown-menu__item"
:class="{
'is-disabled':
visitedRoutes.indexOf(hoverRoute) ===
visitedRoutes.length - 1,
}"
@click="closeRightTabs"
>
<vab-icon icon="arrow-right-line" />
<span>{{ translate('关闭右侧') }}</span>
</li>
<li class="el-dropdown-menu__item" @click="closeAllTabs">
<vab-icon icon="close-line" />
<span>{{ translate('关闭全部') }}</span>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
@use 'sass:math';
.vab-tabs {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
min-height: $base-tabs-height;
padding-right: $base-padding;
padding-left: $base-padding;
user-select: none;
background: var(--el-color-white);
border-top: 1px solid #f6f6f6;
:deep() {
.fold-unfold {
margin-right: $base-margin;
}
[class*='ri'] {
margin-right: 3px;
}
.vab-icon {
vertical-align: -3px;
}
}
&-content {
width: calc(100% - 40px);
&-card {
height: #{$base-tag-item-height};
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height};
line-height: #{$base-tag-item-height};
}
.el-tabs__header {
margin: 0 0 1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
box-sizing: border-box;
height: #{$base-tag-item-height};
margin-top: 1px;
margin-right: 5px;
line-height: #{$base-tag-item-height};
border: 1px solid #{$base-border-color} !important;
border-radius: #{$base-border-radius};
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-active {
color: var(--el-color-primary);
outline: none;
background: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary);
}
&:hover {
border: 1px solid var(--el-color-primary);
}
}
}
}
}
&-smart {
height: #{$base-tag-item-height};
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height};
line-height: #{$base-tag-item-height};
}
.el-tabs__header {
margin: 0 0 1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
height: #{$base-tag-item-height};
margin-top: 1px;
margin-right: 5px;
line-height: #{$base-tag-item-height};
outline: none;
border: 0;
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-active {
outline: none;
background: var(--el-color-primary-light-9);
&:after {
width: 100%;
transition: #{$base-transition};
}
}
&:after {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
content: '';
background-color: var(--el-color-primary);
transition: #{$base-transition};
}
&:hover {
background: var(--el-color-primary-light-9);
&:after {
width: 100%;
transition: #{$base-transition};
}
}
}
}
}
}
&-smooth {
height: #{$base-tag-item-height} + 4;
:deep() {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: #{$base-tag-item-height} + 14;
line-height: #{$base-tag-item-height} + 14;
}
.el-tabs__header {
margin: 0 0 -1px 0;
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
height: #{$base-tag-item-height} + 4;
padding: 0 30px 0 30px;
margin-top: math.div(
$base-tabs-height - $base-tag-item-height - 3.1,
2
);
margin-right: -18px;
line-height: #{$base-tag-item-height} + 4;
text-align: center;
outline: none;
border: 0;
transition: padding 0.3s
cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-closable:hover {
padding: 0 30px 0 30px;
}
&.is-active {
padding: 0 30px 0 30px;
color: var(--el-color-primary);
outline: none;
background: var(--el-color-primary-light-9);
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
&:hover {
padding: 0 30px 0 30px;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
}
&.is-closable {
padding: 0 30px 0 30px;
}
}
&:hover {
padding: 0 30px 0 30px;
color: var(--el-color-black);
background: #dee1e6;
mask: url('~@/assets/tabs_images/vab-tab.png');
mask-size: 100% 100%;
}
}
}
}
}
}
.contextmenu {
position: fixed;
top: 0;
left: 0;
z-index: 10;
.el-dropdown-menu__item:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
}
&-more {
position: relative;
box-sizing: border-box;
display: block;
text-align: left;
&-active,
&:hover {
&:after {
position: absolute;
bottom: 0;
left: 0;
height: 0;
content: '';
}
.vab-tabs-more-icon {
transform: rotate(90deg);
.box-t {
&:before {
transform: rotate(45deg);
}
}
.box:before,
.box:after {
background: var(--el-color-primary);
}
}
}
&-icon {
display: inline-block;
color: #9a9a9a;
cursor: pointer;
transition: transform 0.3s ease-out;
.box {
position: relative;
display: block;
width: 14px;
height: 8px;
&:before {
position: absolute;
top: 2px;
left: 0;
width: 6px;
height: 6px;
content: '';
background: #9a9a9a;
}
&:after {
position: absolute;
top: 2px;
left: 8px;
width: 6px;
height: 6px;
content: '';
background: #9a9a9a;
}
}
.box-t {
&:before {
transition: transform 0.3s ease-out 0.3s;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,515 @@
<script lang="ts" setup>
import { translate } from '@/i18n'
import { useSettingsStore } from '@/store/modules/settings'
const $sub: any = inject('$sub')
const $unsub: any = inject('$unsub')
const $baseLoading: any = inject('$baseLoading')
const settingsStore: any = useSettingsStore()
const { theme, device }: any = storeToRefs(settingsStore)
const { saveTheme, resetTheme, updateTheme }: any = settingsStore
const state = reactive({
drawerVisible: false,
})
const handleOpenTheme = () => {
state.drawerVisible = true
}
const updateMenuWidth = () => {
useCssVar('--el-left-menu-width', ref(null)).value =
theme.value.menuWidth
}
const setDefaultTheme = async () => {
await resetTheme()
await updateTheme()
state.drawerVisible = false
if (document.body.getBoundingClientRect().width - 1 < 992)
location.reload()
}
const handleSaveTheme = async () => {
await saveTheme()
state.drawerVisible = false
if (document.body.getBoundingClientRect().width - 1 < 992)
location.reload()
}
const shuffle = (val: string | boolean, list: (string | boolean)[]) =>
list.filter((_) => _ !== val)[(Math.random() * (list.length - 1)) | 0]
const randomTheme = async () => {
const loading = $baseLoading(0)
// 随机换肤重置移除主题,防止代码更新影响样式
await resetTheme()
theme.value.themeName = shuffle(theme.value.themeName, [
'blue-black',
'blue-white',
'ocean',
'green-black',
'green-white',
'purple-black',
'purple-white',
'red-black',
'red-white',
])
theme.value.columnStyle = shuffle(theme.value.columnStyle, [
'vertical',
'horizontal',
'card',
'arrow',
])
// theme.value.background = shuffle(theme.value.background, [
// 'none',
// 'vab-background',
// ])
theme.value.tabsBarStyle = shuffle(theme.value.tabsBarStyle, [
'card',
'smart',
'smooth',
])
theme.value.showTabsIcon = shuffle(theme.value.showTabsIcon, [
true,
false,
])
theme.value.layout =
device.value === 'desktop'
? shuffle(theme.value.layout, [
'horizontal',
'vertical',
'column',
'comprehensive',
'common',
'float',
])
: 'vertical'
try {
await updateTheme()
await saveTheme()
} finally {
setTimeout(() => {
loading.close()
}, 1000)
}
}
$sub('theme', () => {
handleOpenTheme()
})
$sub('random-theme', () => {
randomTheme()
})
onBeforeUnmount(() => {
$unsub('theme')
$unsub('random-theme')
})
</script>
<template>
<el-drawer
v-model="state.drawerVisible"
append-to-body
class="vab-drawer"
direction="rtl"
size="285px"
:title="translate('主题配置')"
>
<el-scrollbar height="88vh">
<el-form ref="form" label-position="left" :model="theme">
<el-form-item>
<template #label>
{{ translate('布局') }}
<el-tooltip
:content="
translate(
'布局配置仅' +
'在电脑视窗下生效,手机视窗时将' +
'默认锁定为纵向布局'
)
"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.layout"
:disabled="device === 'mobile'"
placeholder="请选择"
>
<el-option
key="column"
:label="translate('分栏')"
value="column"
/>
<el-option
key="comprehensive"
:label="translate('综合')"
value="comprehensive"
/>
<el-option
key="vertical"
:label="translate('纵向')"
value="vertical"
/>
<el-option
key="horizontal"
:label="translate('横向')"
value="horizontal"
/>
<el-option
key="common"
:label="translate('常规')"
value="common"
/>
<el-option
key="float"
:label="translate('浮动')"
value="float"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('主题')">
<el-select v-model="theme.themeName" @change="updateTheme">
<el-option
key="blue-black"
:label="translate('蓝黑')"
value="blue-black"
/>
<el-option
key="blue-white"
:label="translate('蓝白')"
value="blue-white"
/>
<el-option
key="green-black"
:label="translate('绿黑')"
value="green-black"
/>
<el-option
key="green-white"
:label="translate('绿白')"
value="green-white"
/>
<el-option
key="purple-black"
:label="translate('紫黑')"
value="purple-black"
/>
<el-option
key="purple-white"
:label="translate('紫白')"
value="purple-white"
/>
<!-- 红黑红白主题完成群文档任务免费获取 -->
<el-option
key="red-black"
:label="translate('红黑')"
value="red-black"
/>
<el-option
key="red-white"
:label="translate('红白')"
value="red-white"
/>
<el-option
key="ocean"
:label="translate('渐变')"
value="ocean"
/>
</el-select>
</el-form-item>
<el-form-item class="vab-item-custom">
<template #label>
{{ translate('菜单背景') }}
<el-tooltip
:content="
translate(
'支持纵向布局' +
'、分栏布局、综合' +
'布局、常规布局,不支持横' +
'向布局、浮动' +
'布局'
)
"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-radio-group
v-model="theme.background"
:disabled="
theme.layout === 'float' ||
theme.layout === 'horizontal'
"
@change="updateTheme"
>
<el-radio-button
class="none"
label="none"
value="none"
/>
<el-radio-button
class="vab-background"
label="vab-background"
value="vab-background"
/>
</el-radio-group>
</el-form-item>
<el-form-item :label="translate('菜单宽度')">
<el-select
v-model="theme.menuWidth"
:disabled="
theme.layout === 'float' ||
theme.layout === 'horizontal'
"
@change="updateMenuWidth"
>
<el-option key="266px" label="266px" value="266px" />
<el-option key="277px" label="277px" value="277px" />
<el-option key="288px" label="288px" value="288px" />
</el-select>
</el-form-item>
<el-form-item :label="translate('标签')">
<el-switch v-model="theme.showTabs" />
</el-form-item>
<el-form-item>
<template #label>
{{ translate('标签图标') }}
<el-tooltip
:content="translate('标签开启时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-switch
v-model="theme.showTabsIcon"
:disabled="!theme.showTabs"
/>
</el-form-item>
<el-form-item>
<template #label>
{{ translate('标签风格') }}
<el-tooltip
:content="translate('标签开启时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.tabsBarStyle"
:disabled="!theme.showTabs"
>
<el-option
key="card"
:label="translate('卡片')"
value="card"
/>
<el-option
key="smart"
:label="translate('灵动')"
value="smart"
/>
<el-option
key="smooth"
:label="translate('圆滑')"
value="smooth"
/>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
{{ translate('分栏风格') }}
<el-tooltip
:content="translate('分栏布局时生效')"
effect="dark"
placement="top"
>
<vab-icon icon="question-line" />
</el-tooltip>
</template>
<el-select
v-model="theme.columnStyle"
:disabled="theme.layout !== 'column'"
>
<el-option
key="vertical"
:label="translate('纵向')"
value="vertical"
/>
<el-option
key="horizontal"
:label="translate('横向')"
value="horizontal"
/>
<el-option
key="card"
:label="translate('卡片')"
value="card"
/>
<el-option
key="arrow"
:label="translate('箭头')"
value="arrow"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('头部固定')">
<el-switch
v-model="theme.fixedHeader"
:disabled="theme.layout === 'common'"
/>
</el-form-item>
<el-form-item :label="translate('国际化')">
<el-switch v-model="theme.showLanguage" />
</el-form-item>
<el-form-item :label="translate('进度条')">
<el-switch v-model="theme.showProgressBar" />
</el-form-item>
<el-form-item :label="translate('刷新')">
<el-switch v-model="theme.showRefresh" />
</el-form-item>
<el-form-item :label="translate('搜索')">
<el-switch v-model="theme.showSearch" />
</el-form-item>
<el-form-item :label="translate('通知')">
<el-switch v-model="theme.showNotice" />
</el-form-item>
<el-form-item :label="translate('全屏')">
<el-switch v-model="theme.showFullScreen" />
</el-form-item>
<el-form-item :label="translate('锁屏')">
<el-switch v-model="theme.showLock" />
</el-form-item>
<el-form-item :label="translate('页面动画')">
<el-switch v-model="theme.showPageTransition" />
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<el-button type="primary" @click="handleSaveTheme">
{{ translate('保存') }}
</el-button>
<el-button @click="setDefaultTheme">
{{ translate('恢复默认') }}
</el-button>
</template>
</el-drawer>
</template>
<style lang="scss">
@use 'sass:math';
.vab-drawer {
// position: relative;
.el-drawer__header {
padding: $base-padding $base-padding 0 $base-padding;
margin-bottom: 0;
}
.el-drawer__body {
padding-right: 0;
.el-scrollbar__wrap {
height: calc(100vh - 80px);
padding-right: $base-padding;
.el-divider--horizontal {
margin: $base-margin * 2 0 $base-margin * 2 0;
}
.el-form-item {
display: flex;
align-items: center;
&__label {
flex: 1 1;
i {
cursor: pointer;
}
}
&__content {
flex: 0 0 auto;
}
&.vab-item-custom {
display: block !important;
height: 130px;
}
.el-radio-button {
float: left;
display: block;
width: 80px;
height: 80px;
margin: 10px;
cursor: pointer;
border-radius: 5px;
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
&.is-active {
box-shadow: 0 0 2px 2px #1890ff;
}
.el-radio-button__orig-radio,
.el-radio-button__inner {
display: none;
}
&.none {
font-family: 'remixicon', sans-serif !important;
font-size: 16px;
font-weight: 580;
line-height: 80px;
text-align: center;
background: #f7f7f7 none;
background-size: cover;
&:before {
content: '\eace';
}
}
&.vab-background {
background: url(~@/assets/theme_images/background-1.png);
background-size: cover;
}
}
.el-input__inner {
width: 115px;
}
}
}
}
.el-drawer__footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index + 1;
display: flex;
padding: math.div($base-padding, 2);
background: var(--el-color-white);
border-top: 1px solid #{$base-border-color};
}
}
</style>

View File

@@ -0,0 +1,168 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
import { translate } from '@/i18n'
const $pub: any = inject('$pub')
//const $baseMessage: any = inject('$baseMessage')
const settingsStore: any = useSettingsStore()
const { theme }: any = storeToRefs(settingsStore)
const handleOpenTheme = () => {
$pub('theme')
}
const randomTheme = () => {
$pub('random-theme')
}
const buy = () => {
window.open('https://vuejs-core.cn/authorization')
}
// const getCode = () => {
// if (process.env.NODE_ENV === 'development') $pub('get-code')
// else
// ElMessageBox.prompt(
// '请输入密码(密码在购买时获得跳转后需登录购买时绑定的github账号)',
// '温馨提示',
// {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// }
// )
// .then(({ value }) => {
// if (value !== 'vabp') {
// $baseMessage(
// '密码不正确!',
// 'error',
// 'vab-hey-message-error'
// )
// return
// }
// $pub('get-code')
// })
// .catch(() => {})
// }
const removeLocalStorage = () => {
localStorage.clear()
location.reload()
}
</script>
<template>
<ul v-if="theme.showThemeSetting" class="vab-theme-setting">
<li @click="handleOpenTheme">
<a>
<vab-icon icon="brush-2-line" />
<p>{{ translate('主题配置') }}</p>
</a>
</li>
<li @click="randomTheme">
<a>
<vab-icon icon="apps-line" />
<p>{{ translate('随机换肤') }}</p>
</a>
</li>
<li @click="buy">
<a>
<vab-icon icon="shopping-cart-2-line" />
<p>{{ translate('购买源码') }}</p>
</a>
</li>
<!-- <li @click="getCode">
<a>
<vab-icon icon="file-copy-line" />
<p>{{ translate('拷贝源码') }}</p>
</a>
</li> -->
<li @click="removeLocalStorage">
<a>
<vab-icon icon="delete-bin-4-line" />
<p>
{{ translate('清理缓存') }}
</p>
</a>
</li>
</ul>
</template>
<style lang="scss" scoped>
.vab-theme-setting {
position: fixed;
top: 50%;
right: 0;
z-index: $base-z-index - 2;
padding: 10px 0 0 0;
margin: 0;
text-align: center;
cursor: pointer;
background: var(--el-color-white);
border: 1px solid #{$base-border-color};
border-top-left-radius: $base-border-radius + 3;
border-bottom-left-radius: $base-border-radius + 3;
box-shadow: 0 0 50px 0 rgb(82 63 105 / 15%);
transform: translateY(-50%);
> li {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px 10px 10px;
margin: 0;
list-style: none;
&:nth-child(2) {
[class*='ri-'] {
animation: rotate 6s linear infinite;
}
}
$colors: (
1: #3698fd,
2: #1bc3bb,
3: #faa500,
4: #b37feb,
5: #ef4c5d,
);
@each $key, $color in $colors {
&:nth-child(#{$key}) {
a {
color: $color;
background: mix($base-color-white, $color, 90%);
transition:
color 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease,
-webkit-box-shadow 0.15s ease;
&:hover {
color: var(--el-color-white);
background: $color;
}
}
}
}
a {
display: inline-block;
width: 60px;
height: 60px;
padding-top: 10px;
text-align: center;
background: #f6f8f9;
border-radius: $base-border-radius + 3;
p {
padding: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: $base-font-size-small;
line-height: 25px;
white-space: nowrap;
}
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
const $pub: any = inject('$pub')
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const handleOpenTheme = () => {
$pub('theme')
}
</script>
<template>
<span v-if="theme.showTheme">
<vab-icon icon="brush-2-line" @click="handleOpenTheme" />
</span>
</template>

39
library/index.ts Normal file
View File

@@ -0,0 +1,39 @@
import { App } from 'vue'
// 加载雪碧图
import '@/icon'
// 加载全局样式样式
import './styles/vab.scss'
import { createHead } from '@vueuse/head'
// 加载Icon
import VabIcon from 'vab-icons'
import 'vab-icons/lib/vab-icons.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const name = process['env']['VUE_' + 'APP_' + 'GITHUB_' + 'USER_' + 'NAME']
const noTest = name !== 'test'
const noEmpty = name !== 'undefined'
const dev = process['env']['NODE_' + 'ENV'] === 'dev' + 'elop' + 'ment'
export function setupVab(app: App<Element>) {
if ((noTest && noEmpty && !dev && VabIcon) || (dev && VabIcon)) {
app.use(createHead())
app.component('VabIcon', VabIcon)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 加载背景
const Themes = require.context('./styles/background', false, /\.scss$/)
Themes.keys().map(Themes)
// 加载插件
const Plugins = require.context('./plugins', true, /\.ts$/)
Plugins.keys().forEach((key) => {
app.use(Plugins(key).default)
})
}
}

View File

@@ -0,0 +1,78 @@
<!--分栏布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
</script>
<template>
<div
class="vab-layout-column"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-column-bar />
<div
class="vab-main"
:class="{
['vab-main-' + theme.columnStyle]: true,
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-column {
.vab-main {
&.is-collapse-main {
&.vab-main-horizontal {
margin-left: $base-left-menu-width-min * 1.3;
:deep() {
.fixed-header {
width: calc(
100% - #{$base-left-menu-width-min} * 1.3
);
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<!--常规布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-common"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-header layout="common" />
<div>
<vab-side-bar layout="common" />
<div
v-show="showTabs"
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<vab-tabs layout="common" />
</div>
</div>
</div>
<div
class="vab-main main-padding"
:class="{
'is-collapse-main': collapse,
}"
>
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-common {
:deep() {
.vab-tabs-content {
width: calc(
100% - 60px - #{$base-font-size-default} -
#{$base-padding} - 2px
) !important;
}
.vab-header {
.vab-main {
width: 100%;
margin: auto $base-margin;
}
}
}
}
</style>

View File

@@ -0,0 +1,58 @@
<!--综合布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-comprehensive"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar layout="comprehensive" />
<div
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav layout="comprehensive" />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>

View File

@@ -0,0 +1,98 @@
<!--浮动布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
})
const settingsStore = useSettingsStore()
const { foldSideBar, openSideBar } = settingsStore
foldSideBar()
onUnmounted(() => {
openSideBar()
})
</script>
<template>
<div
class="vab-layout-float"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar layout="float" />
<div class="vab-main">
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav layout="float" />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<!--由于element-plus
bug使用teleported=false会导致多级路由无法显示故所有菜单必须生成至body下样式必须放到body下-->
<style lang="scss" scoped>
.vab-layout-float {
:deep() {
.vab-main {
margin-left: $base-left-menu-width-min !important;
.fixed-header {
width: $base-right-content-width-min !important;
}
}
.el-menu--collapse.el-menu li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
.vab-menu-children-height {
height: auto !important;
}
.el-menu {
&--vertical {
.el-menu--popup-right-start {
width: 335px !important;
.el-sub-menu__title,
.el-menu-item {
float: left;
width: 160px;
min-width: 160px;
margin: 0 0 5px 5px;
border-radius: $base-border-radius;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 横向布局 -->
<script lang="ts" setup>
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
</script>
<template>
<div
class="vab-layout-horizontal"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-header layout="horizontal" />
<div
v-show="showTabs"
:class="{
'vab-tabs-horizontal': showTabs,
}"
>
<div class="vab-main">
<vab-tabs />
</div>
</div>
</div>
<div class="vab-main main-padding">
<vab-app-main />
</div>
</div>
</template>
<style lang="scss" scoped>
.vab-layout-horizontal {
:deep() {
.vab-main {
width: 92% !important;
margin: auto !important;
}
}
.vab-tabs-horizontal {
background: var(--el-color-white);
box-shadow: $base-box-shadow;
}
.vab-nav {
.fold-unfold {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<!-- 纵向布局 -->
<script lang="ts" setup>
import { useSettingsStore } from '@/store/modules/settings'
defineProps({
collapse: {
type: Boolean,
default() {
return false
},
},
fixedHeader: {
type: Boolean,
default() {
return true
},
},
showTabs: {
type: Boolean,
default() {
return true
},
},
device: {
type: String,
default() {
return 'desktop'
},
},
})
const settingsStore = useSettingsStore()
const { foldSideBar } = settingsStore
</script>
<template>
<div
class="vab-layout-vertical"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-side-bar />
<div
v-if="device === 'mobile' && !collapse"
class="v-modal"
@click="foldSideBar"
/>
<div
class="vab-main"
:class="{
'is-collapse-main': collapse,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>

162
library/layouts/index.vue Normal file
View File

@@ -0,0 +1,162 @@
<template>
<div class="vue-admin-better-wrapper" :class="{ mobile }">
<component
:is="'vab-layout-' + theme.layout"
:collapse="collapse"
:device="device"
:fixed-header="theme.fixedHeader"
:show-tabs="theme.showTabs"
/>
<el-backtop target="#app" />
<!-- 主题组件放到layouts下防止主题切换导致主题组件重新加载 -->
<vab-theme-drawer />
<vab-theme-setting />
</div>
</template>
<script lang="ts">
import { useSettingsStore } from '@/store/modules/settings'
const imports = require.context('./', true, /\.vue$/)
const Components: any = {}
imports
.keys()
.filter((key) => key !== './index.vue')
.forEach((key) => {
Components[key.replace(/(\/|\.|index.vue)/g, '')] =
imports(key).default
})
export default defineComponent({
name: 'Layouts',
components: Components,
setup() {
const settingsStore = useSettingsStore()
const { device, collapse, theme } = storeToRefs(settingsStore)
const { toggleDevice, foldSideBar, openSideBar, updateTheme } =
settingsStore
const mobile = ref(false)
let oldLayout = theme.value.layout
const resizeBody = () => {
mobile.value =
document.body.getBoundingClientRect().width - 1 < 992
}
watch(mobile, (val) => {
if (val) {
oldLayout = theme.value.layout
foldSideBar()
} else openSideBar()
theme.value.layout = val ? 'vertical' : oldLayout
toggleDevice(val ? 'mobile' : 'desktop')
})
resizeBody()
updateTheme()
const cleanup = useEventListener('resize', () => {
resizeBody()
})
onUnmounted(() => {
if (mobile) theme.value.layout = oldLayout
cleanup()
})
return {
theme,
device,
mobile,
collapse,
foldSideBar,
openSideBar,
toggleDevice,
}
},
})
</script>
<style lang="scss" scoped>
.vue-admin-better-wrapper {
position: relative;
width: 100%;
height: 100%;
[class*='vab-layout-'] {
:deep() {
.vab-layout-header {
box-shadow: $base-box-shadow;
}
}
&.fixed {
padding-top: $base-nav-height + $base-tabs-height;
}
&.fixed.no-tabs-bar {
padding-top: $base-nav-height;
}
}
:deep() {
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: $base-z-index - 1;
width: 100%;
}
.vab-main {
position: relative;
width: auto;
min-height: 100%;
margin-left: var(--el-left-menu-width);
&.is-collapse-main {
margin-left: $base-left-menu-width-min;
.fixed-header {
width: $base-right-content-width-min;
}
}
&:not(.is-collapse-main) {
.fixed-header {
width: calc(100% - var(--el-left-menu-width));
}
}
}
}
/* 手机端开始 */
&.mobile {
:deep() {
.vab-layout-vertical {
.el-scrollbar.vab-side-bar.is-collapse {
width: 0;
}
.vab-main {
.fixed-header {
width: 100%;
}
margin-left: 0;
}
}
/* 隐藏分页和页码跳转 */
.el-pager,
.el-pagination__jump {
display: none;
}
}
}
/* 手机端结束 */
}
</style>

View File

@@ -0,0 +1,19 @@
import type { App, DirectiveBinding } from 'vue'
import { hasPermission } from '@/utils/permission'
export default {
install(app: App<Element>) {
/**
* @description 自定义指令v-permissions
*/
app.directive('permissions', {
mounted(el: any, binding: DirectiveBinding) {
const { value } = binding
if (value)
if (!hasPermission(value))
el.parentNode && el.parentNode.removeChild(el)
},
})
},
}

View File

@@ -0,0 +1,30 @@
import { App } from 'vue'
import pinia from '@/store'
import { useErrorLogStore } from '@/store/modules/errorLog'
import { errorLog } from '@/config'
import { isArray, isString } from '@/utils/validate'
export const needErrorLog = () => {
const errorLogArray = isArray(errorLog)
? [...errorLog]
: isString(errorLog)
? [...[errorLog]]
: []
return errorLogArray.includes(process.env.NODE_ENV as string)
}
export const addErrorLog = (err: any) => {
// eslint-disable-next-line no-console
if (!err.isRequest) console.error('vue-admin-better错误拦截:', err)
const url = window.location.href
const { addErrorLog } = useErrorLogStore(pinia)
addErrorLog({ err, url })
}
export default {
install(app: App<Element>) {
if (needErrorLog()) {
app.config.errorHandler = addErrorLog
}
},
}

View File

@@ -0,0 +1,26 @@
import { App } from 'vue'
import pinia from '@/store'
import { useSettingsStore } from '@/store/modules/settings'
export default {
install(app: App<Element>) {
if (process.env.NODE_ENV !== 'development') {
const { title } = useSettingsStore(pinia)
// eslint-disable-next-line no-console
console.log(
` %c ${title} %c 基于admin-plus ${__APP_INFO__['version']} 构建 `,
'color: #fadfa3; background: #030307; padding:5px 0;',
'background: #fadfa3; padding:5px 0;'
)
}
if (process.env.NODE_ENV !== 'development') {
const str = '\u0076\u0061\u0062\u002d\u0069\u0063\u006f\u006e\u0073'
const key = decodeURI(str.replace(/\\u/g, '%u'))
if (!__APP_INFO__['dependencies'][key]) {
// eslint-disable-next-line
// @ts-ignore
app.config.globalProperties = null
}
}
},
}

172
library/plugins/vab.ts Normal file
View File

@@ -0,0 +1,172 @@
import { App } from 'vue'
import mitt from 'mitt'
import _ from 'lodash'
import { loadingText, messageDuration } from '@/config'
import { globalPropertiesType } from '/#/library'
export let gp: globalPropertiesType
export default {
install(app: App<Element>) {
gp = {
/**
* @description 全局加载层
* @param {number} index 自定义加载图标类名ID
* @param {string} text 显示在加载图标下方的加载文案
*/
$baseLoading: (index = undefined, text = loadingText) => {
return ElLoading.service({
lock: true,
text,
spinner: index ? `vab-loading-type${index}` : index,
background: 'hsla(0,0%,100%,.8)',
})
},
/**
* @description 全局Message
* @param {string} message 消息文字
* @param {'success'|'warning'|'info'|'error'} type 主题
* @param {string} customClass 自定义类名
* @param {boolean} dangerouslyUseHTMLString 是否将message属性作为HTML片段处理
*/
$baseMessage: (
message,
type = 'info',
customClass,
dangerouslyUseHTMLString
) => {
ElMessage({
message,
type,
customClass,
duration: messageDuration,
dangerouslyUseHTMLString,
showClose: true,
})
},
/**
* @description 全局Alert
* @param {string|VNode} content 消息正文内容
* @param {string} title 标题
* @param {function} callback 若不使用Promise,可以使用此参数指定MessageBox关闭后的回调
*/
$baseAlert: (content, title = '温馨提示', callback = undefined) => {
if (title && typeof title == 'function') {
callback = title
title = '温馨提示'
}
ElMessageBox.alert(content, title, {
confirmButtonText: '确定',
dangerouslyUseHTMLString: true, // 此处可能引起跨站攻击建议配置为false
callback: () => {
if (callback) callback()
},
}).then(() => {})
},
/**
* @description 全局Confirm
* @param {string|VNode} content 消息正文内容
* @param {string} title 标题
* @param {function} callback1 确认回调
* @param {function} callback2 关闭或取消回调
* @param {string} confirmButtonText 确定按钮的文本内容
* @param {string} cancelButtonText 取消按钮的自定义类名
*/
$baseConfirm: (
content,
title,
callback1,
callback2,
confirmButtonText = '确定',
cancelButtonText = '取消'
) => {
ElMessageBox.confirm(content, title || '温馨提示', {
confirmButtonText,
cancelButtonText,
closeOnClickModal: false,
type: 'warning',
lockScroll: false,
})
.then(() => {
if (callback1) {
callback1()
}
})
.catch(() => {
if (callback2) {
callback2()
}
})
},
/**
* @description 全局Notification
* @param {string} message 说明文字
* @param {string} title 标题
* @param {'success'|'warning'|'info'|'error'} type 主题样式,如果不在可选值内将被忽略
* @param {'top-right'|'top-left'|'bottom-right'|'bottom-left'} position 自定义弹出位置
* @param duration 显示时间,毫秒
*/
$baseNotify: (
message,
title,
type = 'success',
position = 'top-right',
duration = messageDuration
) => {
ElNotification({
title,
message,
type,
duration,
position,
})
},
/**
* @description 表格高度
* @param {*} formType
*/
$baseTableHeight: (formType) => {
let height = window.innerHeight
const paddingHeight = 291
const formHeight = 60
if ('number' === typeof formType) {
height = height - paddingHeight - formHeight * formType
} else {
height = height - paddingHeight
}
return height
},
$pub: (...args: any[]) => {
_emitter.emit(_.head(args), args[1])
},
$sub: function () {
// eslint-disable-next-line prefer-rest-params
Reflect.apply(_emitter.on, _emitter, _.toArray(arguments))
},
$unsub: function () {
// eslint-disable-next-line prefer-rest-params
Reflect.apply(_emitter.off, _emitter, _.toArray(arguments))
},
}
const _emitter = mitt()
Object.keys(gp).forEach((key) => {
app.provide(key, gp[key as keyof typeof gp])
// 允许vue3下继续使用vue2中的this调用vab方法
app.config.globalProperties[key] = gp[key as keyof typeof gp]
})
if (process.env['NODE_' + 'ENV'] !== `${'deve' + 'lopme' + 'nt'}`) {
const key = 'vab-' + 'icons'
if (!__APP_INFO__['dependencies'][key]) {
// @ts-ignore
app.config.globalProperties = null
}
if (!process.env['VUE_' + 'APP_' + 'SECRET_' + 'KEY']) {
// @ts-ignore
app.config.globalProperties = null
}
}
},
}

12
library/shims-vab.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare let __APP_INFO__: any
// CSS
type CSSModuleClasses = { readonly [key: string]: string }
declare module '*.module.scss' {
const classes: CSSModuleClasses
export default classes
}
declare module '*.scss' {
const css: string
export default css
}

View File

@@ -0,0 +1,251 @@
/**
* @description 黑
*/
body.vab-theme-black {
$base-menu-background: #282c34;
@mixin container {
color: var(--el-color-white) !important;
background: $base-menu-background !important;
}
@mixin active {
&:hover {
color: var(--el-color-white) !important;
background-color: var(--el-color-primary) !important;
}
&.is-active {
color: var(--el-color-white) !important;
background-color: var(--el-color-primary) !important;
}
}
.logo-container-vertical,
.logo-container-horizontal,
.logo-container-comprehensive,
.logo-container-float {
@include container;
}
.logo-container-column {
.logo {
@include container;
}
}
.vab-column-bar-container.el-scrollbar {
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav {
@include container;
}
.el-tabs__item.is-active {
background: var(--el-color-primary) !important;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
}
.vab-column-bar-container-card.el-scrollbar {
.el-tabs {
.el-tabs__item.is-active {
background: transparent !important;
.vab-column-grid {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-column-bar-container-arrow.el-scrollbar {
.el-tabs {
.el-tabs__item.is-active {
background: transparent !important;
.vab-column-grid {
background: transparent !important;
}
}
}
}
.vab-layout-float,
.vab-layout-common,
.vab-layout-vertical,
.vab-layout-horizontal,
.vab-layout-comprehensive {
.el-menu {
@include container;
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include container;
}
}
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu-item {
@include active;
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
}
.vab-header {
@include container;
.vab-main {
@include container;
.right-panel {
.el-menu {
&--horizontal {
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include active;
}
}
}
[role='menubar'].el-menu--horizontal {
> .el-sub-menu.is-active[tabindex='0'] {
> .el-sub-menu__title {
color: var(--el-color-white) !important;
background-color: var(
--el-color-primary
) !important;
}
}
}
}
}
}
.vab-tabs {
&-more {
&-active,
&:hover {
.vab-tabs-more-icon {
.box:before,
.box:after {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-card {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border: 1px solid var(--el-color-primary) !important;
}
&:hover {
border: 1px solid var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-smart {
.el-tabs__header {
.el-tabs__item {
&.is-active {
background: var(--el-color-primary-light-9) !important;
}
&:after {
background-color: var(--el-color-primary) !important;
}
&:hover {
background: var(--el-color-primary-light-9) !important;
}
}
}
}
.vab-tabs-content-smooth {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
&:hover {
color: var(--el-color-primary) !important;
background: var(
--el-color-primary-light-9
) !important;
}
}
&:hover {
color: var(--el-color-black) !important;
}
}
}
}
}
.vab-nav {
.el-tabs__item.is-active,
.el-tabs__item:hover {
color: var(--el-color-primary) !important;
}
.el-tabs__active-bar {
background-color: var(--el-color-primary) !important;
}
}
#nprogress {
.bar {
background: var(--el-color-primary) !important;
}
.peg {
box-shadow:
0 0 10px var(--el-color-primary),
0 0 5px var(--el-color-primary) !important;
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* @description 菜单背景
*/
body.vab-background > #app {
$base-menu-background: url('~@/assets/theme_images/background-1.png')
no-repeat;
@mixin container {
color: $base-color-white !important;
background: $base-menu-background !important;
background-size: auto 100% !important;
}
@mixin transparent {
color: $base-color-white !important;
background: transparent !important;
}
@mixin active {
span {
color: $base-color-white !important;
}
&:hover {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
&.is-active {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
.vab-side-bar:not(.is-collapse),
.comprehensive-bar-container {
@include container;
.el-menu {
@include transparent;
.el-menu-item,
.el-sub-menu__title {
@include transparent;
@include active;
i,
svg {
@include transparent;
}
}
}
.logo-container-vertical,
.logo-container-comprehensive,
.logo-container-float {
@include transparent;
.logo .vab-icon,
.title {
@include transparent;
}
}
}
.vab-column-bar-container {
&.el-scrollbar {
.logo-container-column {
.logo {
@include container;
background: #034291 !important;
.vab-icon {
@include transparent;
}
}
}
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav,
.el-tabs__item {
@include transparent;
&.is-active {
color: $base-color-white !important;
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
}
&.vab-column-bar-container-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
.vab-column-grid {
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,181 @@
/**
* @description 渐变
*/
body.vab-theme-ocean {
$base-color-blue: #1890ff;
$base-color-blue-active: #399efd;
@mixin container {
background: linear-gradient(to right, #006cff, #399efd) !important;
}
@mixin active {
&:hover {
color: $base-color-white;
background-color: $base-color-blue-active !important;
}
&.is-active {
color: $base-color-white;
background-color: $base-color-blue-active !important;
}
}
.logo-container-horizontal {
background: var(--el-color-primary) !important;
}
.logo-container-vertical,
.logo-container-comprehensive,
.logo-container-float {
@include container;
}
.logo-container-column {
.logo {
@include container;
}
}
.vab-column-bar-container {
.el-tabs {
.el-tabs__nav-wrap.is-left {
@include container;
}
.el-tabs__nav {
@include container;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
&-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
}
}
}
}
}
.vab-layout-horizontal {
.vab-header {
background: var(--el-color-primary) !important;
}
.el-menu {
background: var(--el-color-primary) !important;
.el-sub-menu__title {
background: var(--el-color-primary) !important;
}
.el-menu-item {
background: var(--el-color-primary) !important;
}
}
.vab-side-bar,
.comprehensive-bar-container {
background: var(--el-color-primary) !important;
.el-menu-item {
@include active;
}
}
}
.vab-layout-vertical,
.vab-layout-comprehensive,
.vab-layout-common,
.vab-layout-float {
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu {
@include container;
@include active;
.el-sub-menu__title,
.el-menu-item {
background-color: transparent !important;
@include active;
&.is-active {
background-color: $base-color-blue-active !important;
}
}
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
background-color: var(--el-color-primary) !important;
}
}
}
.vab-header {
background-color: var(--el-color-primary) !important;
.vab-main {
.el-menu.el-menu {
background-color: var(--el-color-primary) !important;
&--horizontal {
.el-sub-menu,
.el-menu-item {
background-color: var(--el-color-primary) !important;
&.is-active {
color: $base-color-white !important;
background-color: $base-color-blue-active !important;
}
}
> .el-menu-item,
.el-sub-menu__title,
> .el-menu-item:hover,
> .el-sub-menu__title:hover {
color: $base-color-white !important;
background-color: var(--el-color-primary) !important;
i {
color: $base-color-white !important;
}
&.is-active {
color: $base-color-white !important;
background-color: $base-color-blue-active !important;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,293 @@
/**
* @description 白
*/
body.vab-theme-white {
$base-menu-background: #fff;
@mixin container {
color: #515a6e !important;
background: $base-menu-background !important;
}
@mixin active {
&:hover {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
i,
svg,
span[title] {
color: var(--el-color-primary) !important;
}
}
&.is-active {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
i,
svg,
span[title] {
color: var(--el-color-primary) !important;
}
}
}
.logo-container-common,
.logo-container-vertical,
.logo-container-horizontal,
.logo-container-comprehensive,
.logo-container-float {
@include container;
.title,
.vab-icon {
@include container;
}
}
.logo-container-column {
@include container;
.title {
@include container;
}
.logo,
.vab-icon {
@include container;
}
}
.vab-column-bar-container {
.el-tabs {
@include container;
.el-tabs__nav-wrap.is-left {
background: #f7faff !important;
}
.el-tabs__item,
.el-tabs__nav {
@include container;
}
.el-tabs__item.is-active {
color: var(--el-color-white) !important;
background: var(--el-color-primary) !important;
}
}
.el-menu {
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
&-card {
.el-tabs {
.el-tabs__item {
&.is-active {
background: transparent !important;
}
}
}
}
&-arrow {
.el-tabs {
.el-tabs__item {
&.is-active {
color: var(--el-color-black) !important;
background: transparent !important;
.vab-column-grid {
background: transparent !important;
}
}
}
}
}
}
.vab-layout-float,
.vab-layout-common,
.vab-layout-vertical,
.vab-layout-horizontal,
.vab-layout-comprehensive {
.el-menu {
@include container;
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include container;
}
.el-menu-item.is-active,
.el-sub-menu__title.is-active,
.el-menu-item:hover,
.el-sub-menu__title:hover {
i {
color: var(--el-color-primary) !important;
}
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
.vab-side-bar,
.comprehensive-bar-container {
@include container;
.el-menu-item {
@include active;
}
}
}
.vab-layout-float {
.el-scrollbar__view
.el-menu--collapse.el-menu
li.el-sub-menu.is-active {
.el-sub-menu__title {
background-color: transparent !important;
}
> .el-sub-menu__title {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
}
}
}
.vab-header {
@include container;
.vab-main {
@include container;
.right-panel {
.user-name,
.user-name *,
> i,
> div > i,
> span > i,
> div > span > i,
> svg,
> div > svg,
> span > svg,
> div > span > svg,
.ri-notification-line,
.ri-translate,
.ri-bug-line {
@include container;
}
.el-menu {
&--horizontal {
.el-sub-menu .el-sub-menu__title,
.el-menu-item {
@include active;
}
.el-sub-menu,
.el-menu-item {
&.is-active {
@include active;
}
}
> .el-sub-menu.is-active {
> .el-sub-menu__title {
background-color: var(
--el-color-primary-light-9
) !important;
@include active;
}
}
}
}
}
}
}
.vab-tabs {
&-more {
&-active,
&:hover {
.vab-tabs-more-icon {
.box:before,
.box:after {
background: var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-card {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
border: 1px solid var(--el-color-primary) !important;
}
&:hover {
border: 1px solid var(--el-color-primary) !important;
}
}
}
}
.vab-tabs-content-smart {
.el-tabs__header {
.el-tabs__item {
&.is-active {
background: var(--el-color-primary-light-9) !important;
}
&:after {
background-color: var(--el-color-primary) !important;
}
&:hover {
background: var(--el-color-primary-light-9) !important;
}
}
}
}
.vab-tabs-content-smooth {
.el-tabs__header {
.el-tabs__item {
&.is-active {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
&:hover {
color: var(--el-color-primary) !important;
background: var(
--el-color-primary-light-9
) !important;
}
}
&:hover {
color: var(--el-color-black) !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
.dots-loader:not(:required) {
position: relative;
display: inline-block;
width: 7px;
height: 7px;
margin-bottom: 30px;
overflow: hidden;
text-indent: -9999px;
background: transparent;
border-radius: 100%;
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
transform-origin: 50% 50%;
animation: dots-loader 5s infinite ease-in-out;
}
@keyframes dots-loader {
0% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
8.33% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
16.67% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 14px 14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
25% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
33.33% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae -14px -14px 0 7px;
}
41.67% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
50% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
58.33% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 -14px 14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
66.67% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 -14px -14px 0 7px,
#6d7 -14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
75% {
box-shadow:
#f86 14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px -14px 0 7px,
#4ae 14px -14px 0 7px;
}
83.33% {
box-shadow:
#f86 14px 14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae 14px 14px 0 7px;
}
91.67% {
box-shadow:
#f86 -14px 14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
100% {
box-shadow:
#f86 -14px -14px 0 7px,
#fc6 14px -14px 0 7px,
#6d7 14px 14px 0 7px,
#4ae -14px 14px 0 7px;
}
}

View File

@@ -0,0 +1,104 @@
.gauge-loader:not(:required) {
position: relative;
display: inline-block;
width: 64px;
height: 32px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #6ca;
border-top-left-radius: 32px;
border-top-right-radius: 32px;
}
.gauge-loader:not(:required)::before {
position: absolute;
top: 5px;
left: 30px;
width: 4px;
height: 27px;
content: '';
background: white;
border-radius: 2px;
transform-origin: 50% 100%;
animation: gauge-loader 4000ms infinite ease;
}
.gauge-loader:not(:required)::after {
position: absolute;
top: 26px;
left: 26px;
width: 13px;
height: 13px;
content: '';
background: white;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
}
@keyframes gauge-loader {
0% {
transform: rotate(-50deg);
}
10% {
transform: rotate(20deg);
}
20% {
transform: rotate(60deg);
}
24% {
transform: rotate(60deg);
}
40% {
transform: rotate(-20deg);
}
54% {
transform: rotate(70deg);
}
56% {
transform: rotate(78deg);
}
58% {
transform: rotate(73deg);
}
60% {
transform: rotate(75deg);
}
62% {
transform: rotate(70deg);
}
70% {
transform: rotate(-20deg);
}
80% {
transform: rotate(20deg);
}
83% {
transform: rotate(25deg);
}
86% {
transform: rotate(20deg);
}
89% {
transform: rotate(25deg);
}
100% {
transform: rotate(-50deg);
}
}

View File

@@ -0,0 +1,51 @@
.inner-circles-loader:not(:required) {
position: relative;
display: inline-block;
width: 50px;
height: 50px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: rgba(25, 165, 152, 0.5);
border-radius: 50%;
transform: translate3d(0, 0, 0);
}
.inner-circles-loader:not(:required)::before,
.inner-circles-loader:not(:required)::after {
position: absolute;
top: 0;
display: inline-block;
width: 50px;
height: 50px;
content: '';
border-radius: 50%;
}
.inner-circles-loader:not(:required)::before {
left: 0;
background: #c7efcf;
transform-origin: 0 50%;
animation: inner-circles-loader 3s infinite;
}
.inner-circles-loader:not(:required)::after {
right: 0;
background: #eef5db;
transform-origin: 100% 50%;
animation: inner-circles-loader 3s 0.2s reverse infinite;
}
@keyframes inner-circles-loader {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(360deg);
}
100% {
transform: rotate(0deg);
}
}

View File

@@ -0,0 +1,341 @@
.plus-loader:not(:required) {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #f86;
-moz-border-radius: 24px;
-webkit-border-radius: 24px;
border-radius: 24px;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
-moz-animation: plus-loader-background 3s infinite ease-in-out;
-webkit-animation: plus-loader-background 3s infinite ease-in-out;
animation: plus-loader-background 3s infinite ease-in-out;
}
.plus-loader:not(:required)::after {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #f86;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-top 3s infinite linear;
-webkit-animation: plus-loader-top 3s infinite linear;
animation: plus-loader-top 3s infinite linear;
}
.plus-loader:not(:required)::before {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #fc6;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-bottom 3s infinite linear;
-webkit-animation: plus-loader-bottom 3s infinite linear;
animation: plus-loader-bottom 3s infinite linear;
}
@keyframes plus-loader-top {
2.5% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
13.75% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
13.76% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
25% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
27.5% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
41.25% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
41.26% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
50% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
}
52.5% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
63.75% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
63.76% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
75% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
77.5% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
91.25% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
91.26% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-bottom {
0% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
50% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
75% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
100% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-background {
0% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
}
25% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
27.5% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
50% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
52.5% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
}
75% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
77.5% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
}
100% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}

377
library/styles/normalize.scss vendored Normal file
View File

@@ -0,0 +1,377 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline dotted;
/* 2 */
border-bottom: none;
/* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0;
/* 2 */
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
color: inherit;
/* 2 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
/* 1 */
font: inherit;
-webkit-appearance: button;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -0,0 +1,39 @@
/**
* @description vue过渡动画
*/
.fade-transform {
&-leave-active,
&-enter-active {
transition: $base-transition;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
.no-transform {
&-leave-active,
&-enter-active {
transition: none;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
/**
* @description 旋转动画
*/
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}

633
library/styles/vab.scss Normal file
View File

@@ -0,0 +1,633 @@
/**
* @description 全局样式
*/
@import 'element-plus/theme-chalk/display.css';
@import './normalize';
@import './transition';
@mixin base-scrollbar {
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: mix($base-color-white, $base-menu-background, 90%);
border: 3px solid transparent;
border-radius: 7px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: mix($base-color-white, $base-menu-background, 80%);
}
}
.vab-layout-header,
[class*='-bar-container'] {
transition: $base-transition;
* {
transition: $base-transition;
}
}
html {
body,
body[class*='vab-theme-'] {
position: relative;
box-sizing: border-box;
height: 100vh;
padding: 0;
overflow: hidden;
font-family: 'PingFang SC', Arial, 'Microsoft YaHei', sans-serif;
font-size: $base-font-size-default;
color: var(--el-color-black);
background: $base-color-background;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
#app {
height: 100vh;
overflow: auto;
@include base-scrollbar;
.vab-main {
transition: $base-transition;
.vab-app-main {
width: 100%;
padding: $base-padding;
overflow: hidden;
transition: $base-transition;
>section {
background: var(--el-color-white);
transition: $base-transition;
>[class*='-container'] {
min-height: $base-keep-alive-height;
padding: $base-padding;
background: var(--el-color-white);
transition: $base-transition;
}
}
}
}
}
* {
box-sizing: border-box;
outline: none !important;
@include base-scrollbar;
}
[class*='ri-'] {
vertical-align: -3px !important;
}
.vab-icon {
margin: 0 3px 0 0 !important;
}
/*a标签 */
a {
color: var(--el-color-primary);
text-decoration: none;
}
/*图片 */
img {
object-fit: cover;
&[src=''],
&:not([src]) {
opacity: 0;
}
}
/* vab-fullscreen全屏 */
.vab-fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
z-index: $base-z-index + 3 !important;
box-sizing: border-box !important;
width: 100vw !important;
height: 100vh !important;
padding-bottom: 15px !important;
overflow: auto !important;
}
/* vab-dropdown下拉动画 */
.vab-dropdown {
transition: $base-transition;
&-active {
transform: rotateZ(180deg);
}
}
/* vab-dot圆点动画 */
.vab-dot {
position: relative;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 3px;
vertical-align: middle;
border-radius: 50%;
span {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
animation: vabDot 1.2s ease-in-out infinite;
@keyframes vabDot {
0% {
opacity: 0.6;
transform: scale(0.8);
}
to {
opacity: 0;
transform: scale(2.4);
}
}
}
&-success {
background: var(--el-color-success);
span {
background: var(--el-color-success);
}
}
&-error {
background: var(--el-color-error);
span {
background: var(--el-color-error);
}
}
}
/* vab-data-empty占位图 */
.vab-data-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 600px;
margin: auto;
}
/* el-descriptions */
.el-descriptions {
&__title {
padding-left: 10px;
border-left: 5px solid var(--el-color-primary);
}
}
/* el-button按钮 */
.el-button {
border-radius: var(--el-border-radius-base);
&:hover,
&:focus,
&:active,
&.is-disabled {
background-clip: padding-box;
}
&.is-round {
border-radius: var(--el-border-radius-round);
}
&.is-circle {
border-radius: var(--el-border-radius-circle);
}
[class*='el-icon-']+span,
span+[class*='el-icon-'],
[class*='ri-']+span,
span+[class*='ri-'] {
margin-left: 3px;
}
}
/* el-tag */
.el-tag {
border-radius: var(--el-border-radius-base);
&+.el-tag {
margin-left: 10px;
}
&--light:not(&--success, &--info, &--warning, &--danger) {
--el-tag-bg-color: var(--el-color-primary-light-9);
--el-tag-border-color: var(--el-color-primary-light-8);
--el-tag-text-color: var(--el-color-primary);
--el-tag-hover-color: var(--el-color-primary);
}
&--dark:not(&--success, &--info, &--warning, &--danger) {
--el-tag-bg-color: var(--el-color-primary);
--el-tag-border-color: var(--el-color-primary);
--el-tag-hover-color: var(--el-color-primary-2);
}
&.is-round {
border-radius: var(--el-border-radius-round);
}
}
/* .el-select-tags */
.el-select-tags-wrapper {
.el-tag.el-tag {
margin-left: 0px;
}
}
/* el-select */
.el-select {
min-width: 115px;
}
a+a,
/* span + span, */
a+.el-button,
.el-button+a {
margin-left: 10px;
}
.el-drawer__wrapper {
outline: none !important;
* {
outline: none !important;
}
}
/* el-overlay遮罩 */
.el-overlay {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
}
/* el-image-viewer遮罩 */
.el-image-viewer__mask {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
}
/* v-modal遮罩 */
.v-modal {
z-index: $base-z-index;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0.6;
//backdrop-filter: blur(10px);
}
/* el-loading-mask遮罩 */
.el-loading-mask {
z-index: $base-z-index - 10 !important;
&.is-fullscreen {
z-index: $base-z-index + 99 !important;
}
}
/* el-scrollbar滚动条 */
.el-scrollbar {
height: 100%;
&__bar {
z-index: 999;
}
&__thumb {
background-color: mix($base-color-white,
$base-menu-background,
90%);
&:hover {
background-color: mix($base-color-white,
$base-menu-background,
80%);
}
}
}
/* el-form表单 */
.el-form--label-top {
.el-form-item__label {
padding: 0;
}
}
.el-form-item__label {
padding: 0 10px 0 0;
}
.el-range-editor--small {
.el-range__icon,
.el-range__close-icon {
line-height: 23.5px;
}
}
/* el-badge */
.el-badge__content {
border: 0;
}
/* .el-page-header */
.el-page-header {
margin: 0 0 $base-margin 0;
}
/* el-alert */
.el-alert {
margin: 0 0 $base-margin 0;
&__closebtn {
position: absolute !important;
}
&--success.is-light {
color: var(--el-color-success);
background-color: var(--el-color-success-lighter);
border: 1px solid var(--el-color-success);
i {
color: var(--el-color-success);
}
}
&--info.is-light {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary);
i {
color: var(--el-color-primary);
}
}
&--warning.is-light {
color: var(--el-color-warning);
background-color: var(--el-color-warning-lighter);
border: 1px solid var(--el-color-warning);
i {
color: var(--el-color-warning);
}
}
&--error.is-light {
color: var(--el-color-error);
background-color: var(--el-color-error-lighter);
border: 1px solid var(--el-color-error);
i {
color: var(--el-color-error);
}
}
}
/* el-divider间隔线 */
.el-divider--horizontal {
margin: 8px 0 $base-margin + 8px 0;
.el-divider__text {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
}
/* nprogress进度条 */
#nprogress {
position: fixed;
z-index: $base-z-index + 3;
.bar {
background: var(--el-color-primary);
}
.peg {
box-shadow:
0 0 10px var(--el-color-primary),
0 0 5px var(--el-color-primary);
}
}
/* el-table表格 */
.el-table {
.el-table__body-wrapper {
@include base-scrollbar;
}
th {
background: #f5f7fa !important;
}
td,
th {
position: relative;
box-sizing: border-box;
.cell {
font-size: $base-font-size-default;
font-weight: normal;
color: #606266;
.el-image {
width: 50px;
height: 50px;
border-radius: $base-border-radius;
}
}
}
}
/* el-pagination分页 */
.el-pagination {
justify-content: center;
margin: $base-margin 0 0 0;
font-weight: normal;
color: var(--el-color-black);
}
/* el-menu菜单开始 */
.el-menu,
.vab-column-grid {
user-select: none;
/* plus处理图标间距 */
div,
li,
span {
i+span {
margin-left: 3px;
}
}
&.vab-column-grid-card,
&.vab-column-grid-vertical {
div {
i+span {
margin-left: 0;
}
}
}
}
.el-dialog__body {
padding-right: 30px;
}
/* el-dialog、el-message-box、el-popover、el-button、el-tag */
@media (max-width: 576px) {
.el-dialog,
.el-message-box,
.el-popover.el-popper {
width: 95% !important;
}
.el-button {
margin-bottom: 10px;
}
}
/* el-card卡片 */
.el-card {
margin-bottom: $base-margin;
border-radius: var(--el-border-radius-base);
&__header {
position: relative;
.card-header-tag {
position: absolute;
top: 15px;
right: 20px;
}
}
&__body {
padding: $base-padding;
}
}
/* .vab-hey-message */
.vab-hey-message {
@mixin vab-hey-message {
padding: 15px;
background-color: var(--el-color-white);
border-color: var(--el-color-white);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
.el-message__content {
padding-right: $base-padding;
color: #34495e;
}
.el-icon-close {
color: #34495e;
&:hover {
opacity: 0.8;
}
}
}
&-info {
@include vab-hey-message;
i {
color: $base-color-grey;
}
}
&-success {
@include vab-hey-message;
i {
color: var(--el-color-success);
}
}
&-warning {
@include vab-hey-message;
i {
color: var(--el-color-warning);
}
}
&-error {
@include vab-hey-message;
i {
color: var(--el-color-error);
}
}
}
/* vab-table-expand */
.vab-table-expand {
padding: $base-padding;
line-height: 30px;
&-title {
display: inline-block;
width: 80px;
font-weight: bold;
}
}
:not(.no-background-container).auto-height-container {
display: flex;
flex-direction: column;
height: var(--el-container-height);
.el-table {
flex: 1;
}
.el-scrollbar {
//margin-right: -20px;
.vab-auto-box {
flex: 1;
width: 100%;
padding: 0 var(--el-padding) 0 0;
}
}
@media (max-width: 1024px) {
height: auto;
}
}
.el-card__body {
overflow: hidden;
flex: none;
}
}
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #1890ff;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #77e19d;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #41b584;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #1890ff;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

View File

@@ -0,0 +1,43 @@
$base-color-primary: #6954f0;
$base-color-success: #13ce66;
$base-color-warning: #ffba00;
$base-color-danger: #ff4d4f;
$base-color-error: #ff4d4f;
$base-color-transition: #1890ff;
:export {
vab-color-grey: $base-color-grey;
vab-color-black: $base-color-black;
vab-color-primary: $base-color-primary;
vab-color-primary-light-1: mix($base-color-white, $base-color-primary, 10%);
vab-color-primary-light-2: mix($base-color-white, $base-color-primary, 20%);
vab-color-primary-light-3: mix($base-color-white, $base-color-primary, 30%);
vab-color-primary-light-4: mix($base-color-white, $base-color-primary, 40%);
vab-color-primary-light-5: mix($base-color-white, $base-color-primary, 50%);
vab-color-primary-light-6: mix($base-color-white, $base-color-primary, 60%);
vab-color-primary-light-7: mix($base-color-white, $base-color-primary, 70%);
vab-color-primary-light-8: mix($base-color-white, $base-color-primary, 80%);
vab-color-primary-light-9: mix($base-color-white, $base-color-primary, 90%);
vab-color-success: $base-color-success;
vab-color-success-light: mix($base-color-white, $base-color-success, 80%);
vab-color-success-lighter: mix($base-color-white, $base-color-success, 90%);
vab-color-warning: $base-color-warning;
vab-color-warning-light: mix($base-color-white, $base-color-warning, 80%);
vab-color-warning-lighter: mix($base-color-white, $base-color-warning, 90%);
vab-color-danger: $base-color-danger;
vab-color-danger-light: mix($base-color-white, $base-color-danger, 80%);
vab-color-danger-lighter: mix($base-color-white, $base-color-danger, 90%);
vab-color-error: $base-color-error;
vab-color-error-light: mix($base-color-white, $base-color-error, 80%);
vab-color-error-lighter: mix($base-color-white, $base-color-error, 90%);
vab-color-info: $base-color-text-secondary;
vab-color-info-light: mix($base-color-white,
$base-color-text-secondary,
80%);
vab-color-info-lighter: mix($base-color-white,
$base-color-text-secondary,
90%);
vab-border-radius-base: 5px;
vab-color-transition: $base-color-transition;
vab-left-menu-width: $base-left-menu-width;
}

Some files were not shown because too many files have changed in this diff Show More