提交 9b5afc18 作者: 郁骅焌

Initial commit

上级
> 1%
last 2 versions
not dead
not ie 11
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
# Vab Shop 系列产品受国家计算机软件著作权保护
# 关于举报盗版侵权:请发送举报材料至我司客服邮箱1204505056@qq.com,一经查实,官司所得收入20%归举报人所有,80%归律师事务所所有。
# Vue Shop Vite 系列产品购买地址:https://vuejs-core.cn/authorization/shop-vite.html
# 1.购买者可将授权后的产品用于任意「符合国家法律法规」的应用平台,禁止用于黄赌毒等危害国家安全与稳定的网站。
# 2.购买主体购买后可用于开发商业项目,不限制域名和项目数量,购买主体不可将源码分享第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 3.购买者务必尊重知识产权,严格保证不恶意传播产品源码、不得直接对授权的产品本身进行二次转售或倒卖、开源、不得对授权的产品进行简单包装后声称为自己的产品等,无论有意或无意,我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 4.购买者不可将vip群文档及资料分享给第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 5.购买者购买项目不可以用来构建存在竞争性质的产品并直接对外销售否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 6.购买者购买项目中的源码(包含全部源码、及部分源码片段)不可以用于任何形式的开源项目,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
# 7.用于公司的项目商用时购买需提供公司名称,用于证明购买过我们的项目来用于商业用途,防范法律风险,我们不会将【购买公司】信息泄漏到互联网或告知第三方。
# 8.用于个人学习需提供姓名、联系方式。
# 9.如用于外包项目,购买者购买项目中的源码不可直接对外出售,npm run build编译后的项目不受限制。
# 10.虚拟物品不支持退货退款。
# 11.最终解释权归vab系列著作权人所有。
# 第1步:请在此处配置您的github用户名
VITE_APP_GITHUB_USER_NAME=test
# 第2步:请在项目根目录新建一个.env.local的新文件,切记是新建空的文件不是直接拷贝.env文件的内容
# 第3步:.env.local的文件只能有一行不可以换行,大概500个字符不要复制漏掉,购买时随邮件邀请函发放,格式如下:VITE_APP_SECRET_KEY=XXXXXXX
# 以下内容不建议修改建议将VUE_APP_SECRET_KEY配置到【.env.local】中
VITE_APP_SECRET_KEY=preview
# 开发环境,VUE_APP_BASE_URL可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
# 此文件修改后需要重启项目
# NODE_ENV禁止修改
NODE_ENV=development
# api接口地址
VITE_APP_BASE_URL=''
\ No newline at end of file
# 生产环境,VUE_APP_BASE_URL可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
# 此文件修改后需要重启项目
# NODE_ENV禁止修改
NODE_ENV=production
# api接口地址
VITE_APP_BASE_URL=''
\ No newline at end of file
# 测试环境,VUE_APP_BASE_URL可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
# 此文件修改后需要重启项目
# NODE_ENV禁止修改
NODE_ENV=production
# api接口地址
VITE_APP_BASE_URL=''
\ No newline at end of file
/library/build/unplugin/components.d.ts
node_modules
src/assets
src/icons
public
dist
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
root: true,
env: {
node: true,
browser: true,
'vue/setup-compiler-macros': true,
},
extends: ['@element-plus/eslint-config', 'plugin:unicorn/recommended'],
globals: {
defineOptions: 'writable',
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaVersion: 2020,
},
rules: {
'@typescript-eslint/ban-ts-comment': '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',
'prettier/prettier': 'off',
'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',
'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',
camelcase: 'off',
'vue/attributes-order': [
'warn',
{
alphabetical: true,
},
],
'vue/component-name-in-template-casing': [
'error',
'kebab-case',
{
registeredComponentsOnly: false,
ignores: [],
},
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'any',
normal: 'any',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'vue/v-on-event-hyphenation': [
'error',
'always',
{
autofix: true,
},
],
},
})
*.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
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"
.DS_Store
node_modules
/dist
/mock/controller/*.mjs
/dev-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
shamefully-hoist=true
strict-peer-dependencies=false
auto-imports.d.ts
components.d.ts
index.html
website.html
{
"[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"docthis.authorName": "chuzhixin 1204505056@qq.com",
"docthis.enableHungarianNotationEvaluation": true,
"docthis.includeAuthorTag": true,
"docthis.includeDescriptionTag": true,
"docthis.inferTypesFromNames": true,
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit",
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.quickSuggestions": { "strings": true },
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.tabSize": 2,
"emmet.triggerExpansionOnTab": true,
"explorer.confirmDelete": false,
"files.autoSave": "onFocusChange",
"files.eol": "\n",
"files.exclude": { "**/.idea": true },
"git.autofetch": true,
"git.confirmSync": false,
"git.enableSmartCommit": true,
"javascript.format.enable": true,
"javascript.format.semicolons": "remove",
"javascript.updateImportsOnFileMove.enabled": "always",
"liveServer.settings.donotShowInfoMsg": true,
"path-intellisense.mappings": { "@": "${workspaceRoot}/src" },
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.vueIndentScriptAndStyle": true,
"stylelint.validate": ["vue", "scss"],
"typescript.format.semicolons": "remove",
"typescript.updateImportsOnFileMove.enabled": "always",
"vue.codeActions.savingTimeLimit": 100000,
"workbench.colorTheme": "One Monokai",
"cSpell.words": [
"actived",
"aliyun",
"appinstalled",
"autofix",
"autoresize",
"axios",
"backtop",
"beforeinstallprompt",
"beian",
"bilibili",
"Boxplot",
"brotli",
"btns",
"cascader",
"catched",
"chuzhixin",
"cnpm",
"codepen",
"commafy",
"cropdata",
"cropend",
"croppreview",
"cropvisible",
"ctitle",
"curveness",
"daterange",
"datetime",
"datetimerange",
"douban",
"doubao",
"echarts",
"gantt",
"gitee",
"globle",
"headerbtn",
"imengyu",
"jsencrypt",
"kangc",
"lllustration",
"lllustrations",
"logicflow",
"Mbps",
"messagebox",
"mockjs",
"MSIE",
"nocheck",
"nprogress",
"onwarn",
"openai",
"Openeds",
"opentiny",
"picocolors",
"pinia",
"popconfirm",
"qianwen",
"RBAC",
"remixicon",
"Sankey",
"sortablejs",
"stylelint",
"sundan",
"taobao",
"textareas",
"Tongyiqianwen",
"treemap",
"typeit",
"unplugin",
"unref",
"unsub",
"vcode",
"vite",
"vitebar",
"vuejs",
"vueuse",
"wangeditor",
"wechat",
"weibo",
"xfyun",
"xgplayer",
"xinghuo",
"yiyan",
"zhangwenjia",
"zlevel",
"zoomer",
"zxwk"
],
"i18n-ally.localesPaths": [
"src/i18n"
]
}
<div align="center">
<img width="200" src="https://gcore.jsdelivr.net/gh/zxwk1998/image/logo/vite.svg" alt="VAB"/>
<h1>shop-vite</h1>
</div>
## 🔈 关于 shop-vite
- shop-vite 接口及使用规范继承 admin plus,有过 admin-plus 开发经验的用户可快速上手。
- shop-vite 发布时间较短,不代表最终品质,后续会持续进行更新,敬请期待。
- shop-vite 对比 admin-plus 由于底层脚手架不同,故部分代码无法与 admin-plus 通用。
## 🔈 框架使用建议
- 使用前请一定先阅读 vip 群文档,一般在群公告前 5 条
- 如果您经过翻阅文档、百度后努力尝试仍无法解决问题,可通过 vip 群寻求帮助,讨论时间法定工作日 10 点-16 点
- 对于热心回答群内其他成员问题的用户,所提建议将优先被采纳,并可获得部分内测版本体验资格
- 关于举报盗版侵权:请发送举报材料至<fanhuihui1998@126.com>,一经查实,官司所得收入 20%归举报人所有,80%归律师事务所所有。
- 关于客服人员满意度评价以及相关建议:请发送材料至<fanhuihui1998@126.com>,邮件标题:满意度评价,邮件正文:评价依据,我们必将认真对待每一位客户的诉求。
- 关于 bug 反馈:请发送材料至<fanhuihui1998@126.com>,邮件标题:bug 反馈,邮件正文:bug 截图及描述。
## 🔈 框架使用约定
- 1.购买者可将授权后的产品用于任意「符合国家法律法规」的应用平台,禁止用于黄赌毒等危害国家安全与稳定的网站,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 2.购买主体购买后可用于开发商业项目,不限制域名和项目数量,购买主体不可将源码分享第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 3.购买者务必尊重知识产权,严格保证不恶意传播产品源码、不得直接对授权的产品本身进行二次转售或倒卖、开源、不得对授权的产品进行简单包装后声称为自己的产品等,无论有意或无意,我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 4.购买者不可将 vip 群文档及资料分享给第三方,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 5.购买者购买项目不可以用来构建存在竞争性质的产品并直接对外销售否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 6.购买者购买项目中的源码(包含全部源码、及部分源码片段)不可以用于任何形式的开源项目,否则我们有权利收回产品授权及更新权限,并根据事态轻重追究相应法律责任。
- 7.购买者用于公司的项目商用时购买必须提供公司名称,用于证明购买过我们的项目来进行商业用途,防范法律风险,我们承诺对购买公司信息信息严格保密,不会泄漏到互联网或用于产品宣传。
- 8.购买者用于个人学习需提供姓名、手机联系方式进行实名认证,如无法提供请勿下单。
- 9.如用于外包项目,购买者购买项目中的源码不可直接对外出售,npm run build 编译后的项目不受限制。
- 10.如果您的公司基于 Shop Vite 系列自行研发的产品(如 OA、ERP、SASS
等)需对外销售,并且产品中包含我们框架的前端源码,那么您无法购买以上版本,需联系客服购买专属定制版本(不为第三方提供前端框架代码请忽略本条)。
- 11.虚拟物品下单后不支持退货退款。
- 12.购买者需遵守以上约定,最终解释权归 vab 系列著作权人所有,如果您无法遵守以上约定,请勿下单。
```txt
注:以上协议以 https://vuejs-core.cn/authorization/shop-vite.html 为准
```
## 🔗 链接
- 💻 常规版演示地址:[shop-vite](https://vuejs-core.cn/shop-vite/)
- 📝 使用文档:(文档地址及密码请查看 vip 群群公告第一条)
- 📌 付费版及 vip 群购买地址:[购买地址](https://vuejs-core.cn/authorization/shop-vite.html)
## ✅ 版权须知
Vab 系列产品受国家计算机软件著作权保护(证书号:软著登字第 7051316 号),
禁止公开及传播产品源文件、二次出售等,
违者将承担相应的法律责任,并影响自身使用。
## 🧑‍💻 增值服务
### vip 群
- 每位购买 Shop Vite 的用户均可获得 1 个免费的 vip 互助群免费入群资格,可反馈 bug、协助框架问题解答,无需额外购买
- 免费名额之外,额外加入 vip 群 (100/人 仅限已购买框架的的公司员工加入,购买后联系 微信 zxwk-bfq 即可)
- [购买地址,网页右下角切换付款码即可](https://vuejs-core.cn/authorization/shop-vite.html)
### 定制开发
- 承接各类基于 vab 开发的前端项目
- 承接项目范围 3K+ 至 无上限
- 支持签订合同
- 支持提供发票
- 结算流程:前期款(50%)- 中期款(30%)- 尾款(20%)
- 联系方式:见当前页底部
### 企业一对一远程培训
- 承接一对一远程培训服务(支持提供发票)
- 承接时间: 周一至周六上午 10 点 - 晚上 10 点
- 价格:400 - 10000
- 承接方式:单次、包月、包年
- 联系方式:见当前页底部
### 个人一对一技术指导
- 承接时间: 周一至周六上午 10 点 - 晚上 10 点
- 价格:400-800
- 承接方式:单日
- 支持零基础远程教学(学员需学习刻苦,有上进心)
- 学员需完成老师布置的任务
- 联系方式:见当前页底部
### 联系方式
```txt
邮箱:fanhuihui1998@126.com
邮件标题:企业一对一远程培训 - 公司名称,定制开发 - 公司名称,一对一技术支持 - 公司名称
邮件内容:大致描述 + 联系方式 + 预估需要时间 + 预算
后续: 收到邮件后,工作人员会于第一时间回复
```
#!/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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="webkit" name="renderer"/>
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<meta content="no-cache, no-store, must-revalidate" http-equiv="Cache-Control">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon-180x180.png" rel="apple-touch-icon">
<title>Vue Shop Vite</title>
<meta content="Vue Shop Vite 是一款国内领先的基于Vue3 + Vite5 + Element Plus的中后台Admin前端框架" name="description">
<link href="/static/css/loading.css" rel="stylesheet">
</head>
<body>
<noscript>https://vuejs-core.cn/shop-vite</noscript>
<div id="app">
<figure>
<div class="dot white"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</figure>
</div>
<script src="/src/main.ts" type="module"></script>
</body>
</html>
import banner from 'vite-plugin-banner'
export const createBanner = () => {
return [
banner(
` build: \u0056\u0075\u0065\u0020\u0053\u0068\u006f\u0070\u0020\u0056\u0069\u0074\u0065 \n copyright: \u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0076\u0075\u0065\u006a\u0073\u002d\u0063\u006f\u0072\u0065\u002e\u0063\u006e\u002f\u0073\u0068\u006f\u0070\u002d\u0076\u0069\u0074\u0065 \n time: ${process.env.VITE_APP_UPDATE_TIME} \n`
),
] as any
}
import compressPlugin from 'vite-plugin-compression'
export const createCompress = (compress: any) => {
if (compress === 'brotli') {
return compressPlugin({
ext: '.br',
algorithm: 'brotliCompress',
})
}
if (compress === 'gzip' || compress) {
return compressPlugin({
ext: '.gz',
})
}
return []
}
import basicSsl from '@vitejs/plugin-basic-ssl'
export const createHttps = () => {
return basicSsl()
}
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import chokidar from 'chokidar'
import dayjs from 'dayjs'
import pc from 'picocolors'
import type { Plugin } from 'vite'
import { createBanner } from './banner/'
import { createCompress } from './compress/'
import { createHttps } from './https'
import { createMock } from './mock/'
import { createProgress } from './progress/'
import { createPwa } from './pwa/'
import { createSvgIcons } from './svgSprite/'
import { createUnPlugin } from './unplugin/'
import { createVisualizer } from './visualizer/'
import { compress, https, localEnabled, port, prodEnabled, pwa, pwaDev, report } from '/@/config/'
const viteApp = 'VITE_' + 'APP_'
const viteUser = 'VITE_' + 'USER_'
export const createVitePlugin = (env: Record<string, string>) => {
const vitePlugins: (Plugin | Plugin[])[] = [vue()]
const userName = env[`${viteApp}GITHUB_USER_NAME`]
const secretKey = env[`${viteApp}SECRET_KEY`]
const nodeEnv = env[`${viteUser}NODE_ENV`]
const isEmpty = (value: any) => {
return value == undefined || value == '' || value == null
}
if (isEmpty(userName) || isEmpty(secretKey)) return
if (nodeEnv !== 'development' && (isEmpty(userName) || isEmpty(secretKey))) return
vitePlugins.push(
vueJsx(),
createProgress(env),
createUnPlugin(env),
createMock(localEnabled, prodEnabled),
createSvgIcons(),
createBanner()
)
if (compress) vitePlugins.push(createCompress(compress))
if (pwa) vitePlugins.push(createPwa(nodeEnv, pwaDev))
if (https) vitePlugins.push(createHttps())
if (report) vitePlugins.push(createVisualizer())
return vitePlugins
}
export const createWatch = (env: Record<string, string>) => {
//为了防止新同事忘记配置授权码而造成项目无法打包,请保留以下提示
const userName = env[`${viteApp}GITHUB_USER_NAME`]
const secretKey = env[`${viteApp}SECRET_KEY`]
const nodeEnv = env[`${viteUser}NODE_ENV`]
if (nodeEnv === 'production' && (userName === 'test' || secretKey === 'preview')) {
console.log(
`${pc.red(
'检测到您的用户名或key未配置,key在购买时通过邮件邀请函发放,如您已购买请仔细阅读文档并进行配置,配置完成后方可打包使用。购买地址:https://vuejs-core.cn/authorization/shop-vite.html'
)}`
)
process.exit()
}
if (nodeEnv === 'development') {
chokidar.watch('./src/views').on('change', (path) => {
if (path.endsWith('vue')) {
console.log(
`\n${pc.gray(dayjs().format('HH:mm:ss'))} ${pc.cyan('[Vue Sh' + 'op Vite]')} ${pc.cyan(`http://localhost:${port}/`)} ${pc.green(
'update success'
)} `
)
}
})
}
}
import { viteMockServe } from 'vite-plugin-mock'
export const createMock = (localEnabled: boolean, prodEnabled: boolean) => {
return viteMockServe({
logger: false,
ignore: /^index/,
localEnabled,
prodEnabled,
injectCode: `
import { setupProdMockServer } from '/mock/index'
setupProdMockServer()
`,
})
}
import progress from 'vite-plugin-vitebar'
export const createProgress = (env: Record<string, string>) => {
const projectName = 'Vue Shop Vite'
return progress({ env, projectName })
}
import { VitePWA } from 'vite-plugin-pwa'
export const createPwa = (nodeEnv: string, pwaDev: boolean) => {
return VitePWA({
base: nodeEnv === 'development' && pwaDev ? '/' : './', // ./ 或 /
registerType: 'autoUpdate', // promp弹窗提示手动更新、autoUpdate自动更新,建议使用自动更新
workbox: {
cleanupOutdatedCaches: true,
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
},
devOptions: {
enabled: pwaDev,
},
manifest: {
lang: 'zh',
name: 'Vue Shop Vite',
short_name: 'Vue Shop Vite',
description: 'Vue Shop Vite官网、文档、演示地址',
background_color: '#ffffff',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-64x64.png',
sizes: '64x64',
type: 'image/png',
},
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: 'maskable-icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
})
}
import path from 'node:path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export const createSvgIcons = () => {
return createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/icon')],
symbolId: 'vab-icon-[name]',
// svgoOptions: false,
})
}
/**
* @description: 动态导入components
* @author sundan
*/
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { unplugin } from 'vite-plugin-unplugin'
export const createUnPlugin = (env: Record<string, string>) => {
return unplugin({
env,
imports: [
'vue',
'pinia',
'vue-i18n',
'vue-router',
'@vueuse/core',
{
axios: [['default', 'axios']],
// '/@/i18n': [['translate']],
},
],
resolvers: [
ElementPlusResolver({
importStyle: false, // 必须设置false,否则会严重影响开发体验
}),
],
dirs: [
// 将公用组件放到library/components文件夹下即可实现自动导入
// 一定要注意导入的越多网页加载也慢,如无必要请勿修改此配置
],
})
}
import { visualizer } from 'rollup-plugin-visualizer'
export const createVisualizer: any = () => {
return visualizer({
filename: 'stats.html',
title: 'Rollup Stats',
gzipSize: true,
brotliSize: true,
emitFile: true,
})
}
<template>
<el-alert
:center="props.center"
:closable="props.closable"
:close-text="props.closeText"
:description="props.description"
:effect="props.effect"
:show-icon="props.showIcon"
:title="props.title"
:type="props.type"
v-bind="$attrs"
>
<template v-if="props.title || $slots.title" #title>
<slot name="title">
{{ props.title }}
</slot>
</template>
<template v-if="$slots.default || props.description" #default>
<slot name="default">
{{ props.description }}
</slot>
</template>
</el-alert>
</template>
<script lang="ts" setup>
import { ElAlert } from 'element-plus'
defineOptions({
name: 'VabAlert',
})
const props = defineProps({
...ElAlert.props,
closable: {
type: Boolean,
default: false,
},
})
</script>
<template>
<el-config-provider :button="{ autoInsertSpace: true }" :locale="locale">
<router-view />
<vab-update v-if="pwa" />
</el-config-provider>
</template>
<script lang="ts" setup>
import { pwa } from '/@/config'
import { enLocale, zhLocale } from '/@/i18n'
defineOptions({
name: 'VabApp',
})
const { locale: language } = useI18n()
const locale = computed(() => (language.value === 'en' ? enLocale : zhLocale))
</script>
<template>
<div class="vab-app-main">
<section>
<vab-router-view />
<vab-footer />
</section>
</div>
</template>
<script lang="ts" setup>
import { useRoutesStore } from '/@/store/modules/routes'
import { handleActivePath } from '/@/utils/routes'
defineOptions({
name: 'VabAppMain',
})
const route = useRoute()
const routesStore = useRoutesStore()
const { tab, activeMenu } = storeToRefs(routesStore)
watch(
route,
() => {
if (tab.value.data !== route.matched[0].name) tab.value.data = route.matched[0].name as string
activeMenu.value.data = handleActivePath(route)
},
{ immediate: true }
)
</script>
<template>
<el-popover
v-model:visible="visible"
class="vab-avatar"
popper-class="vab-avatar-popper"
width="188"
@hide="handleShow"
@show="handleHide"
>
<template #reference>
<div class="avatar-dropdown">
<el-avatar class="user-avatar" :src="avatar" />
<div class="username">
<span class="hidden-xs-only">{{ username }}</span>
<vab-icon class="vab-dropdown" :class="{ 'vab-dropdown-active': active }" icon="arrow-down-s-line" />
</div>
</div>
</template>
<template #default>
<div class="avatar-dropdown" @click="handleCommand('personalCenter')">
<el-avatar class="user-avatar" :src="avatar" />
<div class="username">
<div>{{ username }}</div>
<div class="personal-center">
<el-text size="small" type="info">个人中心</el-text>
</div>
</div>
</div>
<el-divider />
<ul class="el-dropdown-menu">
<li class="el-dropdown-menu__item" @click="handleCommand('changeLog')">
<vab-icon icon="file-word-line" />
<span>{{ translate('更新日志') }}</span>
<el-tag effect="dark" size="small" type="danger">99+</el-tag>
</li>
<li class="el-dropdown-menu__item" @click="handleCommand('dataScreen')">
<vab-icon icon="database-2-line" />
<span>{{ translate('数据大屏') }}</span>
</li>
<li class="el-dropdown-menu__item" @click="handleCommand('portal')">
<vab-icon icon="building-line" />
<span>{{ translate('门户') }}</span>
</li>
<li class="el-dropdown-menu__item" @click="handleCommand('book')">
<vab-icon icon="book-2-line" />
<span>{{ translate('文档') }}</span>
</li>
<li class="el-dropdown-menu__item" @click="handleCommand('logout')">
<vab-icon icon="logout-circle-r-line" />
<span>{{ translate('退出登录') }}</span>
</li>
</ul>
</template>
</el-popover>
</template>
<script lang="ts" setup>
import { translate } from '/@/i18n'
import { useUserStore } from '/@/store/modules/user'
import { toLoginRoute } from '/@/utils/routes'
defineOptions({
name: 'VabAvatar',
})
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { avatar, username } = storeToRefs(userStore)
const { logout } = userStore
const active = ref<boolean>(false)
const visible = ref<boolean>(false)
const handleShow = () => {
active.value = false
}
const handleHide = () => {
active.value = true
}
const handleCommand = async (command: any) => {
switch (command) {
case 'logout': {
await logout()
await router.push(toLoginRoute(route.fullPath))
visible.value = false
break
}
case 'personalCenter': {
await router.push('/setting/personalCenter')
visible.value = false
break
}
case 'changeLog': {
await router.push('/changeLog')
visible.value = false
break
}
case 'portal': {
await window.open('#/portal')
visible.value = false
break
}
case 'dataScreen': {
await window.open('#/dataScreen')
visible.value = false
break
}
case 'book': {
$baseAlert(
'已购买用户请前往群公告中获取,购买地址:<a target="_blank" href="https://vuejs-core.cn/authorization/shop-vite.html">https://vuejs-core.cn/authorization/shop-vite.html</a>'
)
visible.value = false
break
}
}
}
</script>
<style lang="scss" scoped>
.avatar-dropdown {
display: flex;
align-items: center;
justify-content: center;
justify-items: center;
.user-avatar {
position: relative;
box-sizing: border-box;
width: 40px;
height: 40px;
padding: 8px;
margin-left: 15px;
cursor: pointer;
border-radius: 50%;
&::after {
position: absolute;
right: 3px;
bottom: 3px;
width: 7px;
height: 7px;
content: '';
background: var(--el-color-success);
border: 3px solid var(--el-color-white);
border-radius: 50%;
}
}
.username {
position: relative;
display: flex;
align-content: center;
align-items: center;
width: max-content;
height: 40px;
margin-left: 6px;
line-height: 40px;
cursor: pointer;
[class*='ri-'] {
margin-left: 0 !important;
}
}
}
</style>
<style lang="scss">
.vab-avatar-popper {
padding: 0 !important;
.avatar-dropdown {
display: flex;
flex: 1;
justify-content: start !important;
padding: calc(var(--el-padding) / 1.5);
.user-avatar {
margin-left: calc(var(--el-margin) / 2) calc(var(--el-margin) / 2) calc(var(--el-margin) / 2) 0 !important;
}
.username {
display: flex;
flex-wrap: wrap;
line-height: 20px;
.personal-center {
width: 100%;
font-size: var(--el-font-size-small);
color: var(--el-color-grey);
}
}
}
.el-dropdown-menu {
position: relative;
padding: calc(var(--el-padding) / 2);
&__item {
.el-tag {
position: absolute;
right: 17.5px;
}
}
&__item:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-radius: var(--el-border-radius-base);
}
}
.el-divider--horizontal {
margin: 0;
}
}
</style>
<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 && item.meta.icon" :icon="item.meta.icon" />
<span>{{ translate(item.meta.title) }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script lang="ts" setup>
import { translate } from '/@/i18n'
import { useRoutesStore } from '/@/store/modules/routes'
import { handleMatched } from '/@/utils/routes'
defineOptions({
name: 'VabBreadcrumb',
})
const route = useRoute()
const routesStore = useRoutesStore()
const { getBreadcrumbRoutes: breadcrumbRoutes } = storeToRefs(routesStore)
const breadcrumbList = computed(() => {
const matchedRoutes = handleMatched(breadcrumbRoutes.value, route.fullPath).filter((item) => !item.meta.breadcrumbHidden)
if (matchedRoutes.length > 0) return matchedRoutes
else return handleMatched(breadcrumbRoutes.value, route.path).filter((item) => !item.meta.breadcrumbHidden)
})
const handleTo = (path: any) => {
if (path) return { path }
}
</script>
<style lang="scss" scoped>
.vab-breadcrumb {
display: flex;
align-items: center;
justify-content: center;
height: var(--el-nav-height);
:deep() {
.el-breadcrumb__item {
.el-breadcrumb__inner {
font-weight: normal;
}
}
}
}
</style>
<template>
<el-card :body-class="props.bodyClass" :body-style="props.bodyStyle" class="vab-card" :shadow="props.shadow">
<template v-if="$slots.header || props.title" #header>
<slot v-if="$slots.header" name="header"></slot>
<template v-else>{{ props.title }}</template>
</template>
<el-skeleton v-if="props.skeleton" animated :loading="skeletonShow" :rows="props.skeletonRows">
<template #default>
<slot></slot>
</template>
</el-skeleton>
<slot v-else></slot>
<template v-if="$slots.footer" #footer>
<slot name="footer"></slot>
</template>
</el-card>
</template>
<script lang="ts" setup>
import { ElCard } from 'element-plus'
defineOptions({
name: 'VabCard',
})
const props = defineProps({
...ElCard.props,
shadow: {
type: String,
default: 'never',
},
skeleton: {
type: Boolean,
default: false,
},
skeletonRows: {
type: Number,
default: 5, //显示的数量会比传入的数量多 1
},
title: {
type: String,
default: '',
},
})
const skeletonShow = ref<boolean>(true)
const timer: any = setTimeout(() => {
skeletonShow.value = false
}, 500)
onBeforeUnmount(() => {
if (timer) clearTimeout(timer)
})
</script>
<style lang="scss" scoped>
.vab-card {
:deep() {
.el-card__header {
font-weight: 500;
[class*='ri-'] {
background-image: linear-gradient(120deg, #bd34fe 30%, var(--el-color-primary));
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.el-skeleton {
height: 100%;
overflow: hidden;
}
}
}
</style>
<template>
<div v-if="'technology' != theme.themeName" class="vab-color-picker" style="margin-left: var(--el-margin)">
<el-color-picker
v-model="theme.color"
popper-class="vab-color-picker-popper"
:predefine="predefineColors"
@active-change="handleChange"
/>
</div>
</template>
<script lang="ts" setup>
import { color as _color } from '/@/config/'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabColorPicker',
})
const predefineColors = ref<any>([
_color,
'#1e90ff',
'#4e6ef2',
'#0052d9',
'#3fb884',
'#16baa9',
'#07c160',
'#009688',
'#6954f0',
'#7b40f2',
'#f01414',
])
const settingsStore = useSettingsStore()
const { updateTheme, saveTheme } = settingsStore
const { theme } = storeToRefs(settingsStore)
const handleChange = (value: any) => {
theme.value.color = value
updateTheme()
saveTheme()
}
onBeforeMount(() => {
// 还原默认
$sub('shop-vite-reset-color', () => {
handleChange(_color)
})
})
</script>
<style lang="scss">
.vab-color-picker-popper {
box-sizing: content-box !important;
padding: calc(var(--el-padding) / 2);
.el-color-dropdown__link-btn {
display: none;
}
.el-color-dropdown__btns {
margin-top: 0;
}
}
</style>
<template>
<el-card
:body-style="props.bodyStyle"
class="vab-colorful-card"
:shadow="props.shadow"
:style="
props.style
? props.style
: {
background: `linear-gradient(120deg, ${props.colorFrom} 10%, ${props.colorTo})`,
}
"
>
<template v-if="$slots.header" #header>
<slot name="header"></slot>
</template>
<vab-icon v-if="props.icon" :icon="props.icon" />
<slot></slot>
</el-card>
</template>
<script lang="ts" setup>
import { ElCard } from 'element-plus'
defineOptions({
name: 'VabColorfulCard',
})
const props = defineProps({
...ElCard.props,
shadow: {
type: String,
default: 'never',
},
colorFrom: {
type: String,
default: '',
},
colorTo: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
style: {
type: Object,
default: () => {},
},
})
</script>
<style lang="scss" scoped>
.vab-colorful-card {
position: relative;
min-height: 120px;
cursor: pointer;
:deep() {
.el-card__header {
color: var(--el-color-white);
}
}
i {
position: absolute;
right: 20px;
font-size: 60px;
transform: rotate(15deg);
}
}
</style>
<template>
<el-switch
v-if="'technology' != theme.themeName && 'plain' != theme.themeName && route.path !== '/goods/posterDesign'"
v-model="mode"
:active-icon="Moon"
active-value="dark"
class="vab-dark"
:inactive-icon="Sunny"
inactive-value="light"
inline-prompt
@click="_toggleDark($event)"
/>
</template>
<script lang="ts" setup>
// @ts-nocheck
import { Moon, Sunny } from '@element-plus/icons-vue'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabDark',
})
const route = useRoute()
const settingsStore = useSettingsStore()
const { theme, mode } = storeToRefs(settingsStore)
const { updateMode } = settingsStore
const _toggleDark = async (event: MouseEvent) => {
if (typeof document.startViewTransition === 'function') {
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
let isDark: boolean
const transition = document.startViewTransition(() => {
const root = document.documentElement
isDark = root.classList.contains('dark')
root.classList.remove(isDark ? 'dark' : 'light')
root.classList.add(isDark ? 'light' : 'dark')
handleSetScheme(isDark ? 'light' : 'dark')
})
await transition.ready.then(() => {
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
document.documentElement.animate(
{
clipPath: isDark ? [...clipPath].reverse() : clipPath,
},
{
duration: 500,
easing: 'ease-in',
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
}
)
})
} else {
const toggleDark = useToggle(handleUseDark())
await toggleDark()
}
await updateMode(localStorage.getItem('vueuse-color-scheme'))
}
const handleUseDark = () => {
return useDark()
}
const handleGetScheme = () => {
return localStorage.getItem('vueuse-color-scheme')
}
const handleSetScheme = (value: string) => {
return localStorage.setItem('vueuse-color-scheme', value)
}
onBeforeMount(() => {
// 还原默认
$sub('shop-vite-reset-dark', () => {
mode.value = handleGetScheme()
if (handleGetScheme() === 'dark') {
handleSetScheme('light')
handleUseDark()
mode.value = 'light'
}
})
handleUseDark()
if (handleGetScheme() === 'auto') handleSetScheme('light')
mode.value = handleGetScheme()
})
</script>
<style lang="scss">
::view-transition-old(root),
::view-transition-new(root) {
mix-blend-mode: normal;
animation: none;
}
::view-transition-old(root) {
z-index: 999;
}
::view-transition-new(root) {
z-index: 1;
}
.dark {
&::view-transition-old(root) {
z-index: 1;
}
&::view-transition-new(root) {
z-index: 999;
}
}
.vab-dark {
margin-left: var(--el-margin);
}
</style>
<template>
<el-dialog
v-model="dialogVisible"
:align-center="props.alignCenter"
:append-to="props.appendTo"
:append-to-body="props.appendToBody"
:before-close="props.beforeClose"
:center="props.center"
:class="{
['vab-dialog-' + props.theme]: true,
'open-in-tab': props.openInTab,
}"
:close-on-click-modal="props.closeOnClickModal"
:close-on-press-escape="props.closeOnPressEscape"
:destroy-on-close="props.destroyOnClose"
:draggable="props.draggable"
:fullscreen="isFullscreen"
:lock-scroll="props.lockScroll"
:modal="props.modal"
:modal-class="props.modalClass"
:open-delay="props.openDelay"
:overflow="props.overflow"
:show-close="props.showClose"
:style="{
transition: props.animated ? 'all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),transform 0s' : '',
}"
:top="props.top"
:width="props.width"
v-bind="$attrs"
>
<template #header>
<slot name="header">
<div class="el-dialog__title" @dblclick="setFullscreen">{{ props.title }}</div>
</slot>
<button v-if="props.showClose" class="el-dialog__headerbtn" type="button" @click="closeDialog">
<el-icon class="el-dialog__close">
<close />
</el-icon>
</button>
<button v-if="props.showFullscreen" class="el-dialog__headerbtn" style="right: 51px" type="button" @click="setFullscreen">
<vab-icon class="el-dialog__close el-dialog__fullscreen" :icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'" />
</button>
</template>
<div v-loading="props.loading">
<slot></slot>
</div>
<template #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { Close } from '@element-plus/icons-vue'
import { ElDialog } from 'element-plus'
defineOptions({
name: 'VabDialog',
})
const props = defineProps({
...ElDialog.props,
modelValue: {
type: Boolean,
default: false,
},
showFullscreen: {
type: Boolean,
default: true,
},
fullscreen: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
animated: {
type: Boolean,
default: true,
},
closeOnClickModal: {
type: Boolean,
default: false,
},
closeOnPressEscape: {
type: Boolean,
default: false,
},
theme: {
type: String,
default: 'default', //支持default、plain、primary三种
},
draggable: {
type: Boolean,
default: true,
},
openInTab: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const dialogVisible = useVModel(props, 'modelValue', emit)
const isFullscreen = ref<any>(false)
const closeDialog = () => {
dialogVisible.value = false
isFullscreen.value = false
}
const setFullscreen = () => {
isFullscreen.value = !isFullscreen.value
}
watch(
props,
() => {
isFullscreen.value = props.fullscreen
},
{
immediate: true,
}
)
</script>
<style lang="scss">
.el-overlay:has(.open-in-tab) {
position: absolute;
.el-overlay-dialog {
position: absolute;
}
}
</style>
<template>
<blockquote
v-if="props.blockquote"
class="vab-blockquote"
:class="props.isBorder ? 'vab-blockquote-' + props.type + ' is-border' : 'vab-blockquote-' + props.type"
>
<slot></slot>
</blockquote>
<fieldset v-else-if="props.fieldset" class="vab-fieldset">
<legend>{{ props.title }}</legend>
<slot></slot>
</fieldset>
<el-divider v-else :border-style="props.borderStyle" :content-position="props.contentPosition" :direction="props.direction">
<template #default>
<slot></slot>
</template>
</el-divider>
</template>
<script lang="ts" setup>
import { ElDivider } from 'element-plus'
defineOptions({
name: 'VabDivider',
})
const props = defineProps({
...ElDivider.props,
blockquote: {
type: Boolean,
default: false,
},
fieldset: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'primary', // 类型: primary / success / warning / danger / info
},
isBorder: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
.vab-blockquote {
width: 100%;
padding: 15px;
margin: 0 0 10px 0;
line-height: 1.8;
background-color: var(--el-background-color);
border-left: 5px solid var(--el-color-primary);
border-radius: 0 var(--el-border-radius-base) var(--el-border-radius-base) 0;
&-primary {
border-left: 5px solid var(--el-color-primary);
}
&-success {
border-left: 5px solid var(--el-color-success);
}
&-warning {
border-left: 5px solid var(--el-color-warning);
}
&-danger {
border-left: 5px solid var(--el-color-danger);
}
&-info {
border-left: 5px solid var(--el-color-info);
}
&.is-border {
border-top: 1px solid var(--el-border-color);
border-right: 1px solid var(--el-border-color);
border-bottom: 1px solid var(--el-border-color);
}
}
.vab-fieldset {
padding: var(--el-padding);
margin-bottom: 10px;
border: 1px solid var(--el-border-color);
legend {
padding: 0 var(--el-padding) 0 var(--el-padding);
}
}
</style>
<template>
<span :class="'vab-dot vab-dot-' + type"></span>
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabDot',
})
defineProps({
type: {
values: ['primary', 'success', 'warning', 'danger'],
type: String,
default: 'primary',
},
})
</script>
<style lang="scss" scoped>
/**
* @name: vab-dot
* @description: vab圆点动画
* @author: sundan
* @date: 2024-08-05 22:53:00
*/
.vab-dot {
position: relative;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 3px;
vertical-align: middle;
border-radius: 50%;
@keyframes vabDot {
0% {
opacity: 0.6;
transform: scale(0.8);
}
to {
opacity: 0;
transform: scale(2.4);
}
}
&::after {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
display: block;
width: 100%;
height: 100%;
content: '';
border-radius: 50%;
animation: vabDot 1.2s ease-in-out infinite;
}
@mixin set-color($color) {
&-#{$color} {
background: var(--el-color-#{$color});
&::after {
background: var(--el-color-#{$color});
}
}
}
@include set-color(primary);
@include set-color(success);
@include set-color(warning);
@include set-color(error);
@include set-color(danger);
}
</style>
<template>
<el-table border :data="errorLogs">
<el-table-column label="报错路由">
<template #default="{ row }">
<el-button :href="row.url" rel="noopener noreferrer" tag="a" target="_blank" text type="success">{{ row.url }}</el-button>
</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="操作">
<template #default="{ row }">
<el-tooltip :content="row.err.stack" effect="light">
<el-button text type="primary">错误详情</el-button>
</el-tooltip>
<a>
<el-button
v-for="(item, index) in searchList"
:key="index"
:href="item.url + row.err.message"
rel="noopener noreferrer"
tag="a"
target="_blank"
text
type="primary"
>
{{ item.title }}
</el-button>
</a>
</template>
</el-table-column>
<template #empty>
<el-empty class="vab-data-empty" description="暂无数据" />
</template>
</el-table>
</template>
<script lang="ts" setup>
import { useErrorLogStore } from '/@/store/modules/errorLog'
defineOptions({
name: 'VabErrorLogContent',
})
const errorLogStore = useErrorLogStore()
const { errorLogs } = storeToRefs(errorLogStore)
const searchList = ref<any>([
{
title: '百度搜索',
url: 'https://www.baidu.com/baidu?wd=',
icon: 'baidu-line',
},
{
title: '谷歌搜索',
url: 'https://www.google.com/search?q=',
icon: 'google-line',
},
])
</script>
<template>
<div v-if="errorLogs.length > 0">
<el-badge type="danger" :value="errorLogs.length" @click="dialogVisible = true">
<vab-icon icon="bug-line" />
</el-badge>
<vab-dialog v-model="dialogVisible" append-to-body title="shop-vite 异常捕获" width="60%">
<vab-error-log-content />
<template #footer>
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="danger" @click="clearAll">暂不显示</el-button>
</template>
</vab-dialog>
</div>
</template>
<script lang="ts" setup>
import { useErrorLogStore } from '/@/store/modules/errorLog'
defineOptions({
name: 'VabErrorLog',
})
const errorLogStore = useErrorLogStore()
const { errorLogs } = storeToRefs(errorLogStore)
const { clearErrorLog } = errorLogStore
const dialogVisible = ref<boolean>(false)
const clearAll = () => {
dialogVisible.value = false
clearErrorLog()
}
</script>
<template>
<vab-icon class="fold-unfold" :icon="collapse ? unfold : fold" @click="toggleCollapse" />
</template>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabFold',
})
defineProps({
unfold: {
type: String,
default: 'menu-unfold-line',
},
fold: {
type: String,
default: 'menu-fold-line',
},
})
const settingsStore = useSettingsStore()
const { collapse } = storeToRefs(settingsStore)
const { toggleCollapse } = settingsStore
</script>
<style lang="scss" scoped>
.fold-unfold {
color: var(--el-color-grey);
cursor: pointer;
}
</style>
<template>
<el-dropdown class="vab-language" @command="handleCommand">
<vab-icon icon="font-size-2" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in fontSizeList" :key="item" :command="item">{{ item }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabFontSize',
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const { updateTheme, saveTheme } = settingsStore
const fontSizeList = ref<string[]>(['13px', '13.5px', '14px', '15px', '15.5px', '16px'])
const handleCommand = (fontSize: string) => {
theme.value.fontSize = fontSize
updateTheme()
saveTheme()
}
</script>
<template>
<footer v-if="theme.showFooter" class="vab-footer">
Copyright
<vab-icon icon="copyright-line" />
{{ fullYear }} {{ title }}
<a
v-if="beian"
class="hidden-xs-only"
href="https://beian.miit.gov.cn/#/Integrated/index"
style="margin-left: 3px; color: var(--el-color-grey)"
target="_blank"
>
{{ beian }}
</a>
</footer>
</template>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabFooter',
})
const route = useRoute()
const fullYear = new Date().getFullYear()
const settingsStore = useSettingsStore()
const { title, theme } = storeToRefs(settingsStore)
const beian = ref<any>(localStorage.getItem('beian'))
onBeforeMount(() => {
if (route.query && route.query.beian) {
beian.value = route.query.beian
localStorage.setItem('beian', beian.value)
} else {
// 应对工信部审查,请自行配置成自己的备案号
// 以下网站一年内停用
if (location.hostname.includes('beautiful')) beian.value = ''
// 以下网站为此后官方站点
if (location.hostname.includes('vuejs-core')) beian.value = ''
}
})
</script>
<style lang="scss" scoped>
.vab-footer {
display: flex;
align-items: center;
justify-content: center;
min-height: var(--el-footer-height);
padding: 0 var(--el-padding) 0 var(--el-padding);
margin-top: var(--el-margin);
color: var(--el-color-grey);
background: var(--el-color-white);
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
transition: var(--el-transition);
i {
margin: 0 3px;
}
}
</style>
<template>
<vab-icon class="vab-fullscreen" :icon="isFullscreen ? 'fullscreen-exit-fill' : 'fullscreen-fill'" @click="toggle" />
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabFullscreen',
})
const { isFullscreen, toggle } = useFullscreen()
</script>
<template>
<div class="vab-header">
<div class="vab-main">
<div class="right-panel">
<vab-logo />
<el-menu
v-if="'horizontal' === layout"
active-text-color="var(--el-menu-color-text)"
background-color="var(--el-menu-background-color)"
:default-active="activeMenu.data"
menu-trigger="hover"
mode="horizontal"
text-color="var(--el-menu-color-text)"
>
<vab-menu v-for="(item, index) in handleRoutes" :key="index + item['name']" :item="item" :layout="layout" />
</el-menu>
<vab-right-tools is-horizontal />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRoutesStore } from '/@/store/modules/routes'
defineOptions({
name: 'VabHeader',
})
defineProps({
layout: {
type: String,
default: 'horizontal',
},
})
const routesStore = useRoutesStore()
const { getActiveMenu: activeMenu, getRoutes: routes } = storeToRefs(routesStore)
const handleRoutes = computed(() =>
routes.value.flatMap((route) => (route.meta && route.meta.levelHidden && route.children ? [...route.children] : route))
)
</script>
<style lang="scss">
.vab-header .vab-main .right-panel {
.el-menu.el-menu--horizontal {
width: calc(100vw * 0.92 - 195px - 435px) !important;
}
}
</style>
<style lang="scss" scoped>
.vab-header {
display: flex;
align-items: center;
justify-items: flex-end;
height: var(--el-header-height);
background: var(--el-menu-background-color);
.vab-main {
padding: 0 var(--el-padding) 0 var(--el-padding);
.right-panel {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--el-header-height);
:deep() {
.vab-logo-horizontal {
width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.el-sub-menu__icon-more {
margin-right: var(--el-margin) !important;
}
.el-sub-menu__hide-arrow {
.el-sub-menu__title {
padding-right: 0;
}
}
.el-menu {
&.el-menu--horizontal {
width: 60%;
height: 40px;
border: 0;
* {
border: 0;
}
> .el-menu-item {
border-radius: var(--el-border-radius-base);
&.is-active {
background: var(--el-color-primary) !important;
}
}
}
[class*='ri-'] {
margin-left: 0;
}
}
.username,
.username + i {
color: var(--el-color-white);
}
[class*='ri-'] {
margin-left: var(--el-margin);
color: var(--el-color-white);
}
}
}
}
}
</style>
<style>
.el-popper.is-pure.is-light:has(.el-menu--horizontal, .el-menu--popup-container) {
margin-top: calc(var(--el-margin) * 0.4);
border: 0;
}
</style>
<template>
<el-dropdown class="vab-language" @command="handleCommand">
<vab-icon icon="translate-2" />
<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>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
import getPageTitle from '/@/utils/pageTitle'
defineOptions({
name: 'VabLanguage',
})
const { locale } = useI18n()
const route = useRoute()
const settingsStore = useSettingsStore()
const { changeLanguage } = settingsStore
const handleCommand = (language: string) => {
changeLanguage(language)
locale.value = language
document.title = getPageTitle(route.meta.title)
//@ts-ignore
if (route.path === '/login' || route.path === '/register') location.reload(true)
}
</script>
<template>
<component :is="type" v-bind="linkProps()">
<slot></slot>
</component>
</template>
<script lang="ts" setup>
import { isExternal } from '/@/utils/validate'
defineOptions({
name: 'VabLink',
})
const props = defineProps({
to: {
type: String,
required: true,
},
target: {
type: String,
default: '',
},
})
const type = computed(() => (isExternal(props.to) ? 'a' : 'router-link'))
const linkProps = () =>
isExternal(props.to)
? {
href: props.to,
target: '_blank',
rel: 'noopener',
}
: { to: props.to, target: props.target }
</script>
<template>
<div class="vab-lock">
<vab-icon icon="lock-2-line" @click="handleLock" />
<el-drawer
v-model="lock"
append-to-body
class="vab-lock-drawer"
:close-on-click-modal="false"
:close-on-press-escape="false"
direction="ttb"
:show-close="false"
size="100%"
:with-header="false"
>
<div class="vab-screen-lock">
<div id="vab-screen-lock-background" class="vab-screen-lock-background" :style="style"></div>
<div class="vab-screen-lock-content">
<div class="vab-screen-lock-content-title">
<el-avatar :size="180" :src="avatar" />
<vab-icon icon="lock-2-line" />
{{ title }} {{ translate('屏幕已锁定') }}
</div>
<div class="vab-screen-lock-content-form">
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent>
<el-form-item prop="password">
<el-input v-model="form.password" v-focus autocomplete="off" :placeholder="translate('请输入密码123456')" type="password" />
<el-button native-type="submit" type="primary" @click="handleUnLock">
<vab-icon icon="rotate-lock-2-line" />
<span>{{ translate('解锁') }}</span>
</el-button>
</el-form-item>
</el-form>
</div>
<span @click="randomBackground">{{ translate('切换壁纸') }}</span>
</div>
</div>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { sample, shuffle } from 'lodash-es'
import { translate } from '/@/i18n'
import { useBingStore } from '/@/store/modules/bing'
import { useSettingsStore } from '/@/store/modules/settings'
import { useUserStore } from '/@/store/modules/user'
defineOptions({
name: 'VabLock',
})
const userStore = useUserStore()
const { avatar } = storeToRefs(userStore)
const settingsStore = useSettingsStore()
const { lock, title } = storeToRefs(settingsStore)
const { handleLock, handleUnLock: _handleUnLock } = settingsStore
const bingStore = useBingStore()
const { backgroundList } = storeToRefs(bingStore)
const url = 'https://cdn.jsdelivr.net/gh/chuzhixin/image/vab-image-lock/'
const background = ref<string | undefined>(`${url}${Math.round(Math.random() * 31)}.jpg`)
const style = reactive<any>({
background: 'var(--el-color-primary-light-5)',
backgroundSize: '100%',
filter: 'blur(5px)',
transform: 'scale(1.05)',
transition: 'all 3s ease-in-out',
})
const randomBackground = () => {
style.transform = 'scale(1.05)'
style.transition = 'none'
background.value = sample(shuffle(backgroundList.value))
style.background = `fixed url(${background.value}) center`
setTimeout(() => {
style.transform = 'scale(1.2)'
style.transition = 'all 3s ease-in-out'
}, 0)
}
const validatePass = (rule: any, value: string, callback: any) => {
if (value === '' || value !== '123456') {
callback(new Error('请输入正确的密码'))
} else {
callback()
}
}
const formRef = ref()
const form = reactive({
password: '123456',
})
const rules = {
password: [{ validator: validatePass, trigger: 'blur' }],
}
const handleUnLock = () => {
formRef.value?.validate(async (valid: boolean) => {
if (valid) await _handleUnLock()
})
}
watch(
lock,
() => {
setTimeout(() => {
lock.value ? (style.transform = 'scale(1.2)') : (style.transform = 'scale(1.05)')
}, 500)
},
{
immediate: true,
}
)
onMounted(() => {
setTimeout(() => {
randomBackground()
}, 50)
})
</script>
<style lang="scss">
.el-overlay:has(.vab-lock-drawer) {
backdrop-filter: none;
.vab-lock-drawer {
.el-drawer__body {
padding: 0 !important;
overflow: hidden !important;
}
}
}
</style>
<style lang="scss" scoped>
.vab-lock-drawer {
.vab-screen-lock {
position: relative;
z-index: var(--el-z-index);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 100vw;
height: calc(var(--vh, 1vh) * 100);
font-weight: bold;
background: var(--el-mask-color);
opacity: var(--opacity-value);
&-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: calc(var(--el-z-index) - 1);
}
&-content {
z-index: var(--el-z-index);
width: 400px;
padding: 40px 55px 40px 55px;
color: var(--el-color-grey);
text-align: center;
background: var(--el-mask-color);
backdrop-filter: blur(10px);
border: 1px solid var(--el-border-color);
border-radius: 15px;
> span {
font-size: var(--el-font-size-extra-small);
cursor: pointer;
}
&-title {
line-height: 50px;
color: var(--el-color-grey);
text-align: center;
:deep() {
.el-avatar {
width: 150px;
height: 150px;
img {
padding: 30px;
cursor: pointer;
}
}
[class*='ri-'] {
display: block;
margin: auto !important;
font-size: 30px;
color: var(--el-color-grey) !important;
}
}
}
&-form {
:deep() {
.el-input {
position: relative;
width: 100%;
height: 40px;
line-height: 40px;
&__wrapper {
padding-right: 0;
border: 1px solid var(--el-color-primary);
box-shadow: none;
}
&__inner {
width: 180px;
}
&__suffix {
.el-input__validateIcon {
display: none;
}
}
}
.el-button {
position: absolute;
right: -1px;
z-index: 999;
height: 40px;
margin-left: 0 !important;
line-height: 40px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
[class*='ri-'] {
margin-left: 0 !important;
}
}
}
}
}
@media (max-width: 768px) {
.vab-screen-lock-content {
width: 100% !important;
padding: 40px 35px 40px 35px;
margin: 5vw;
}
}
}
}
</style>
<template>
<div
class="vab-logo"
:class="{
['vab-logo-' + 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>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabLogo',
})
const settingsStore = useSettingsStore()
const { theme, logo, title } = storeToRefs(settingsStore)
</script>
<style lang="scss" scoped>
@mixin container {
position: relative;
height: var(--el-header-height);
overflow: hidden;
line-height: var(--el-header-height);
background: transparent;
}
@mixin logo {
display: inline-block;
width: 32px;
height: 32px;
color: var(--el-title-color);
vertical-align: middle;
fill: currentColor;
}
@mixin title {
display: inline-block;
margin-left: 5px;
overflow: hidden;
font-size: var(--el-font-size-extra-large);
line-height: 55px;
color: var(--el-title-color);
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.vab-logo {
&-horizontal {
@include container;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
}
}
&-vertical,
&-column,
&-comprehensive,
&-fall {
@include container;
height: var(--el-logo-height);
line-height: var(--el-logo-height);
text-align: center;
.logo {
svg,
img {
@include logo;
}
}
.title {
@include title;
max-width: calc(var(--el-left-menu-width) - 60);
}
}
&-column {
background: var(--el-color-white) !important;
.logo {
position: fixed;
top: 0;
display: block;
width: var(--el-left-menu-width-min);
height: var(--el-logo-height);
margin: 0;
background: var(--el-menu-background-color);
}
.title {
position: fixed;
left: var(--el-left-menu-width-min) !important;
box-sizing: border-box;
display: block !important;
width: calc(var(--el-left-menu-width) - var(--el-left-menu-width-min) - 1px);
height: var(--el-nav-height);
margin-left: 0 !important;
color: var(--el-color-grey) !important;
background: var(--el-color-white) !important;
border-bottom: 1px solid var(--el-border-color);
@include title;
}
}
}
</style>
<template>
<el-menu-item :index="itemOrMenu.path" @click="handleLink">
<vab-icon
v-if="itemOrMenu.meta && 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 && itemOrMenu.meta.badge" effect="dark" :type="itemOrMenu.meta.badgeType || 'danger'">
{{ translate(itemOrMenu.meta.badge) }}
</el-tag>
<vab-dot
v-if="itemOrMenu.meta && itemOrMenu.meta.dot"
:type="typeof itemOrMenu.meta.dot === 'string' ? itemOrMenu.meta.dot : 'danger'"
/>
</el-menu-item>
</template>
<script lang="ts" setup>
import { isHashRouterMode } from '/@/config'
import { translate } from '/@/i18n'
import { useSettingsStore } from '/@/store/modules/settings'
import { isExternal } from '/@/utils/validate'
defineOptions({
name: 'VabMenuItem',
})
const props = defineProps({
itemOrMenu: {
type: Object,
default: () => {},
},
})
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const { device } = storeToRefs(settingsStore)
const { foldSideBar } = settingsStore
const { enter, exit } = useFullscreen()
const handleLink = () => {
nextTick(() => {
const routePath = props.itemOrMenu.path
const target = props.itemOrMenu.meta.target
const fullscreen = props.itemOrMenu.meta.fullscreen
if (target === '_blank') {
if (isExternal(routePath)) {
window.open(routePath)
router.push('/redirect')
} else if (route.path !== routePath) isHashRouterMode ? window.open(`#${routePath}`) : window.open(routePath)
router.push('/redirect')
} else {
if (isExternal(routePath)) globalThis.location.href = routePath
else if (route.path !== routePath) {
if (device.value === 'mobile') foldSideBar()
router.push(props.itemOrMenu.path)
}
}
setTimeout(() => {
if (fullscreen) enter()
else exit()
}, 1000)
})
}
</script>
<style lang="scss" scoped>
:deep(.el-tag) {
position: absolute;
right: 20px;
height: 18px;
padding-right: 5px;
padding-left: 5px;
font-size: var(--el-font-size-extra-small);
line-height: 18px;
}
.vab-dot {
position: absolute !important;
right: 20px;
}
</style>
<template>
<template v-if="itemOrMenu.meta && itemOrMenu.meta.levelHidden">
<template v-for="route in itemOrMenu.children" :key="route.path">
<vab-menu :item="route" />
</template>
</template>
<el-sub-menu v-else :index="itemOrMenu.path">
<template #title>
<vab-icon
v-if="itemOrMenu.meta && 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></slot>
</el-sub-menu>
</template>
<script lang="ts" setup>
import { translate } from '/@/i18n'
defineOptions({
name: 'VabSubMenu',
})
defineProps({
itemOrMenu: {
type: Object,
default: () => {},
},
})
</script>
<template>
<component :is="menuComponent" :item-or-menu="item">
<template v-if="item.children && item.children.length > 0">
<vab-menu v-for="route in item.children" :key="route.path" :item="route" />
</template>
</component>
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabMenu',
})
interface ComponentType {
default: Component
}
const imports = import.meta.glob<ComponentType>('./**/*.vue', { eager: true })
const Components: Record<string, Component> = {}
Object.getOwnPropertyNames(imports).forEach((key) => {
Components[key.replaceAll(/(\/|components|\.|vue)/g, '')] = imports[key].default
})
const props = defineProps({
item: {
type: Object,
required: true,
},
layout: {
type: String,
default: '',
},
})
const menuComponent = computed(() =>
props.item.children &&
props.item.children.some((route: any) => {
return route.meta && route.meta.hidden !== true
})
? Components['VabSubMenu']
: Components['VabMenuItem']
)
</script>
<template>
<div class="vab-nav" :class="'vab-nav-' + layout">
<div class="left-panel">
<vab-logo v-if="layout === 'comprehensive'" class="hidden-sm-and-down" />
<vab-fold fold="contract-left-line" unfold="contract-right-line" />
<el-tabs
v-if="layout === 'comprehensive'"
v-model="tab.data"
class="comprehensive-tabs"
tab-position="top"
@tab-click="handleTabClick"
>
<template v-for="item in routes" :key="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" />
{{ translate(item.meta.title) }}
</template>
</el-tab-pane>
</template>
</el-tabs>
<vab-breadcrumb v-else class="hidden-xs-only hidden-md-and-down" />
</div>
<div class="right-panel">
<vab-right-tools />
</div>
</div>
</template>
<script lang="ts" setup>
import { openFirstMenu } from '/@/config'
import { translate } from '/@/i18n'
import { useRoutesStore } from '/@/store/modules/routes'
import { useSettingsStore } from '/@/store/modules/settings'
import { isExternal } from '/@/utils/validate'
defineOptions({
name: 'VabNav',
})
const props = defineProps({
layout: {
type: String,
default: '',
},
})
const router = useRouter()
const routesStore = useRoutesStore()
const { getTab: tab, getTabMenu: tabMenu, getRoutes: routes } = storeToRefs(routesStore)
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const handleTabClick = () => {
nextTick(() => {
if (isExternal(tabMenu.value.path)) {
window.open(tabMenu.value.path)
router.push('/redirect')
} else if (openFirstMenu) router.push(tabMenu.value.redirect || tabMenu.value)
})
}
watch(
() => props.layout,
(val) => {
if (val === 'comprehensive') {
theme.value.fixedHeader = true
}
},
{
immediate: true,
}
)
</script>
<style lang="scss">
.vab-layout-comprehensive {
.vab-side-bar {
top: var(--el-nav-height) !important;
padding-top: 0 !important;
.el-scrollbar__view {
margin-top: calc(0px - var(--el-nav-height) + var(--el-margin) / 2) !important ;
}
}
.comprehensive-tabs {
width: calc(100vw - var(--el-left-menu-width) - 635px) !important;
}
&:has(.is-collapse) {
.fixed-header:has(.vab-nav-comprehensive) {
.vab-tabs {
width: calc(100vw - var(--el-left-menu-width-min)) !important;
margin-left: var(--el-left-menu-width-min) !important;
border-bottom: 1px solid var(--el-border-color) !important;
}
}
}
.fixed-header:has(.vab-nav-comprehensive) {
z-index: calc(var(--el-z-index) + 10) !important;
width: 100vw !important;
border-bottom: 0 !important;
.vab-nav-comprehensive {
border-bottom: 1px solid var(--el-border-color);
}
.vab-logo {
--el-title-color: var(--el-color-black);
width: calc(var(--el-left-menu-width) - var(--el-padding));
}
.vab-tabs {
width: calc(100vw - var(--el-left-menu-width)) !important;
margin-left: var(--el-left-menu-width) !important;
border-top: 0 !important;
border-bottom: 1px solid var(--el-border-color) !important;
}
.comprehensive-tabs {
.el-tabs__item {
padding: 0 15px;
}
.el-tabs__nav-next,
.el-tabs__nav-prev {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
</style>
<style lang="scss" scoped>
.vab-nav {
position: relative;
display: flex;
justify-content: space-between;
height: var(--el-nav-height);
padding-right: var(--el-padding);
padding-left: var(--el-padding);
overflow: hidden;
user-select: none;
background: var(--el-color-white);
border-bottom: 1px solid var(--el-border-color);
.left-panel {
display: flex;
align-items: center;
justify-items: center;
height: var(--el-nav-height);
:deep() {
.fold-unfold {
margin-right: var(--el-margin);
}
.el-tabs {
width: 100%;
margin-left: 0;
.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 {
> div {
display: flex;
align-items: center;
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: var(--el-nav-height);
transition: var(--el-transition);
:deep() {
[class*='ri-'] {
margin-left: var(--el-margin);
color: var(--el-color-grey);
cursor: pointer;
}
button {
[class*='ri-'] {
margin-left: 0;
color: var(--el-color-white);
cursor: pointer;
}
}
}
}
@media (max-width: 480px) {
.right-panel {
:deep() {
.el-badge,
.ri-refresh-line {
display: none;
}
}
}
}
}
</style>
<template>
<el-badge type="danger" :value="badge">
<el-popover placement="bottom" :width="305">
<template #reference>
<vab-icon icon="notification-2-line" />
</template>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane :label="translate('通知')" name="notice">
<div class="notice-list">
<el-scrollbar>
<ul v-if="badge">
<li v-for="(item, index) in notices" :key="index">
<el-avatar :size="45" :src="item.image" />
<span v-html="item.notice"></span>
</li>
</ul>
<el-empty v-else description="暂无数据" />
</el-scrollbar>
</div>
</el-tab-pane>
<el-tab-pane :label="translate('邮件')" name="email">
<div class="notice-list">
<el-scrollbar>
<ul v-if="badge">
<li v-for="(item, index) in notices" :key="index">
<el-avatar :size="45" :src="item.image" />
<span>{{ item.email }}</span>
</li>
</ul>
<el-empty v-else description="暂无数据" />
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
<div class="notice-clear" @click="handleClearNotice">
<el-button text>
<vab-icon icon="close-circle-line" />
<span>{{ translate('清空消息') }}</span>
</el-button>
</div>
</el-popover>
</el-badge>
</template>
<script lang="ts" setup>
import { getList } from '/@/api/notice'
import { translate } from '/@/i18n'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabNotice',
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const activeName = ref<string>('notice')
const notices = ref<Array<any>>([])
const badge = ref<any>(undefined)
const fetchData = async () => {
const { data } = await getList()
notices.value = data.list
badge.value = data.total === 0 ? undefined : data.total
}
const handleClick = () => {
fetchData()
}
const handleClearNotice = () => {
badge.value = ''
notices.value = []
$baseMessage('清空消息成功', 'success', 'hey')
}
onBeforeMount(() => {
if (theme.value.showNotice) fetchData()
})
</script>
<style lang="scss" scoped>
:deep() {
.el-tabs__active-bar {
min-width: 28px;
}
}
.notice-list {
height: 315px;
ul {
padding: 0 15px 0 0;
margin: 0;
li {
display: flex;
align-items: center;
padding: 10px 0 15px 0;
&:hover {
background-color: var(--el-color-primary-light-9);
border-radius: var(--el-border-radius-base);
}
:deep() {
.el-avatar {
flex-shrink: 0;
width: 50px;
height: 50px;
border-radius: 50%;
}
}
span {
margin-left: 10px;
}
}
}
}
.notice-clear {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0 0 0;
font-size: var(--el-font-size-base);
text-align: center;
cursor: pointer;
border-top: 1px solid var(--el-border-color);
}
</style>
<template>
<el-pagination
:background="props.background"
:current-page="props.currentPage"
:default-current-page="props.defaultCurrentPage"
:default-page-size="props.defaultPageSize"
:disabled="props.disabled"
:hide-on-single-page="props.hideOnSinglePage"
:layout="props.layout"
:next-icon="props.nextIcon"
:next-text="props.nextText"
:page-count="props.pageCount"
:page-size="props.pageSize"
:page-sizes="props.pageSizes"
:pager-count="props.pagerCount"
:popper-class="props.popperClass"
:prev-icon="props.prevIcon"
:prev-text="props.prevText"
:small="props.small"
:teleported="props.teleported"
:total="props.total"
v-bind="$attrs"
/>
</template>
<script lang="ts" setup>
import { ElPagination } from 'element-plus'
defineOptions({
name: 'VabPagination',
})
const props = defineProps({
...ElPagination.props,
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
},
background: {
type: Boolean,
default: true,
},
})
</script>
<template>
<el-col :span="24">
<div class="bottom-panel">
<slot></slot>
</div>
</el-col>
</template>
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="left-panel">
<slot></slot>
</div>
</el-col>
</template>
<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="right-panel">
<slot></slot>
</div>
</el-col>
</template>
<script lang="ts" setup>
defineProps({
span: {
type: Number,
default: 10,
},
})
</script>
<template>
<el-col :span="24">
<div class="top-panel">
<slot></slot>
</div>
</el-col>
</template>
<template>
<el-row class="vab-query-form" :gutter="0">
<slot></slot>
</el-row>
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabQueryForm',
})
</script>
<style lang="scss" scoped>
@mixin panel {
display: flex;
flex-wrap: wrap;
align-content: center;
align-items: center;
justify-content: flex-start;
min-height: var(-el-input-height);
margin: 0 0 calc(var(--el-margin) / 2) 0;
.el-form-item__content {
display: flex;
align-items: center;
}
> .el-button {
margin: 0 10px calc(var(--el-margin) / 2) 0 !important;
}
}
.vab-query-form {
:deep() {
.el-input,
.el-select {
width: 175px;
}
.el-form-item:first-child {
margin: 0 0 calc(var(--el-margin) / 2) 0 !important;
}
.el-form-item + .el-form-item {
margin: 0 0 calc(var(--el-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>
<template>
<vab-icon :class="className" icon="refresh-line" @click="refreshRoute" />
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabRefresh',
})
const className = ref<string>('')
const rotate = () => {
className.value = 'rotate'
setTimeout(() => {
className.value = ''
}, 500)
}
const refreshRoute = () => {
$pub('reload-router-view')
rotate()
}
onBeforeMount(() => {
$sub('refresh-rotate', () => {
rotate()
})
})
</script>
<template>
<div class="vab-right-tools">
<vab-search v-show="!isHorizontal" class="hidden-xs-only" />
<div class="vab-right-tools-draggable">
<vab-dark v-show="theme.showDark" :style="!isHorizontal ? '' : { marginLeft: 'var(--el-margin)' }" />
<vab-color-picker v-show="theme.showColorPicker" />
<vab-theme v-show="theme.showTheme && routeName !== 'SeparateLayout'" />
<vab-error-log class="hidden-xs-only" />
<vab-font-size v-show="theme.showFontSize" />
<vab-lock v-show="theme.showLock" />
<vab-notice v-show="theme.showNotice" />
<vab-language v-show="theme.showLanguage" />
<vab-fullscreen v-show="theme.showFullScreen" />
<vab-refresh v-show="theme.showRefresh" />
</div>
<vab-avatar />
</div>
</template>
<script lang="ts" setup>
import Sortable from 'sortablejs'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabRightTools',
})
defineProps({
isHorizontal: {
type: Boolean,
default: false,
},
})
const route = useRoute()
const settingsStore = useSettingsStore()
const { theme, device } = storeToRefs(settingsStore)
const routeName = ref<any>(route.name)
let sortable: any
const handleTabDrag = () => {
if (theme.value.rightToolsDrag && device.value != 'mobile') {
const toolsElement = document.querySelector('.vab-right-tools-draggable') as HTMLElement | null
if (toolsElement)
sortable = new Sortable(toolsElement, {
animation: 150,
easing: 'cubic-bezier(1, 0, 0, 1)',
})
}
}
watch(
route,
() => {
routeName.value = route.name
},
{ immediate: true }
)
onMounted(() => {
nextTick(() => {
handleTabDrag()
})
})
watch(
theme.value,
() => {
if (theme.value.rightToolsDrag) handleTabDrag()
else sortable && sortable.destroy()
},
{
immediate: true,
}
)
</script>
<style lang="scss" scoped>
.vab-right-tools {
display: flex;
align-items: center;
justify-content: flex-end;
&-draggable {
display: flex;
align-items: center;
justify-content: flex-end;
}
}
</style>
<template>
<router-view v-slot="{ Component }">
<transition mode="out-in" :name="theme.pageTransition">
<keep-alive :include="keepAliveNameList" :max="keepAliveMaxNum">
<component :is="Component" :key="routerKey" ref="componentRef" />
</keep-alive>
</transition>
</router-view>
</template>
<script lang="ts" setup>
import { useHead } from '@vueuse/head'
import VabProgress from 'nprogress'
import { keepAliveMaxNum } from '/@/config'
import { useSettingsStore } from '/@/store/modules/settings'
import { useTabsStore } from '/@/store/modules/tabs'
import { handleActivePath } from '/@/utils/routes'
defineOptions({
name: 'VabRouterView',
})
const route = useRoute()
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const tabsStore = useTabsStore()
const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
const componentRef = ref<any>()
const routerKey = ref<any>()
const keepAliveNameList = ref<any>()
const siteData = reactive<any>({
description: '',
})
useHead({
meta: [
{
name: `description`,
content: computed(() => siteData.description),
},
],
})
const updateKeepAliveNameList = (refreshRouteName = null) => {
keepAliveNameList.value = visitedRoutes.value
.filter((item) => !item.meta.noKeepAlive && item.name !== refreshRouteName)
.flatMap((item) => item.name)
}
// 更新KeepAlive缓存页面
watchEffect(() => {
routerKey.value = handleActivePath(route, true)
updateKeepAliveNameList()
siteData.description = `${'Vue'} ${'Shop'} ${'Vite'}-${route.meta.title}简介、官网、首页、文档和下载 - 前端开发框架`
})
onBeforeMount(() => {
$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)
})
})
</script>
<template>
<el-tree-select
v-if="theme.showSearch"
v-model="searchValue"
class="vab-search"
clearable
:data="addFieldToTree(routes)"
default-expand-all
filterable
highlight-current
:prefix-icon="Search"
@node-click="handleSelect"
>
<template #default="{ data }">
<vab-icon v-if="data.meta && data.meta.icon" :icon="data.meta.icon" />
<span style="margin-left: 3px">{{ translate(data.meta.title) }}</span>
</template>
</el-tree-select>
</template>
<script lang="ts" setup>
import { Search } from '@element-plus/icons-vue'
import { isHashRouterMode } from '/@/config'
import { translate } from '/@/i18n'
import { useRoutesStore } from '/@/store/modules/routes'
import { useSettingsStore } from '/@/store/modules/settings'
import { isExternal } from '/@/utils/validate'
defineOptions({
name: 'VabSearch',
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
const searchValue = ref<any>('')
const router = useRouter()
const route = useRoute()
const routesStore = useRoutesStore()
const { getRoutes: routes } = storeToRefs(routesStore)
const addFieldToTree = (routes: any) => {
routes.forEach((node: any) => {
node.value = node.name
node.label = translate(node.meta.title)
if (node.children && node.children.length > 0) addFieldToTree(node.children)
})
return routes
}
const handleSelect = (item: any) => {
nextTick(() => {
if (!item.children)
if (isExternal(item.path)) {
window.open(item.path)
router.push('/redirect')
return
} else if (item.meta.target === '_blank') {
isHashRouterMode ? window.open(`#${item.path}`) : window.open(item.path)
router.push('/redirect')
return
} else router.push(item.path)
})
}
watch(
route,
() => {
if (route.fullPath.includes('?')) {
//处理query传参
const matched = route.fullPath.match(/\?(.*)$/)
const name: any = route.name
if (matched) name.includes('?') ? (searchValue.value = route.name) : (searchValue.value = `${route.name as string}?${matched[1]}`)
// 详情页显示搜索项
if (route.meta.hidden && name.includes('Detail')) searchValue.value = ''
} else searchValue.value = route.name
},
{
immediate: true,
}
)
</script>
<style lang="scss" scoped>
.vab-search {
margin-left: var(--el-margin);
:deep() {
.el-input {
width: 150px !important;
}
}
}
</style>
<template>
<el-scrollbar class="vab-side-bar" :class="{ 'is-collapse': collapse }">
<vab-logo v-if="layout === 'vertical'" class="fixed-logo" />
<el-menu
background-color="var(--el-menu-background-color)"
:collapse="collapse"
:collapse-transition="false"
:default-active="activeMenu.data"
:default-openeds="defaultOpeneds"
menu-trigger="click"
mode="vertical"
text-color="var(--el-menu-color-text)"
:unique-opened="uniqueOpened"
>
<vab-menu v-for="(item, index) in handleRoutes" :key="index + item.name" :item="item" />
</el-menu>
</el-scrollbar>
</template>
<script lang="ts" setup>
import { defaultOpeneds, uniqueOpened } from '/@/config'
import { useRoutesStore } from '/@/store/modules/routes'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabSideBar',
})
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(() =>
props.layout === 'comprehensive'
? partialRoutes.value
: routes.value.flatMap((route: any) => (route.meta.levelHidden && route.children ? [...route.children] : route))
)
</script>
<style lang="scss" scoped>
@mixin active {
&:hover {
color: var(--el-color-white);
background-color: var(--el-color-primary);
}
&.is-active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
}
}
.vab-side-bar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: var(--el-left-menu-width);
overflow: hidden;
background: var(--el-menu-background-color);
transition: var(--el-transition);
.fixed-logo {
position: absolute;
top: 0;
left: 0;
z-index: var(--el-z-index);
width: 100%;
height: var(--el-header-height);
background: var(--el-menu-background-color);
}
&.is-collapse {
z-index: calc(var(--el-z-index) + 1);
width: var(--el-left-menu-width-min);
border-right: 0;
:deep() {
.el-menu {
border-right: 0 !important;
}
.el-menu--collapse.el-menu {
> .el-menu-item,
> .el-sub-menu .el-sub-menu__title {
justify-content: center;
height: calc(var(--el-menu-item-height) - 6px);
padding: 0;
line-height: calc(var(--el-menu-item-height) - 6px);
text-align: center;
[class*='ri'] {
display: block;
padding: 0 !important;
margin: 0 !important;
}
.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-menu.el-menu--vertical {
margin-top: var(--el-header-height);
}
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu-item,
.el-sub-menu__title {
height: var(--el-menu-item-height);
margin: 0 10px 5px 10px;
overflow: hidden;
line-height: var(--el-menu-item-height);
text-overflow: ellipsis;
white-space: nowrap;
border-radius: var(--el-border-radius-base);
@include active;
}
}
}
</style>
<style lang="scss">
.el-menu {
border-right: 0;
}
.el-menu--popup-right-start {
--el-menu-hover-bg-color: var(--el-color-primary) !important;
--el-menu-active-color: var(--el-color-white) !important;
.is-active {
background: var(--el-color-primary) !important;
}
}
</style>
<template>
<div></div>
</template>
<script lang="ts" setup>
// @ts-nocheck
defineOptions({
name: 'VabStatistics',
})
// 网站访问量统计 如不需要请自行注释
onBeforeMount(() => {
if (location.hostname !== 'localhost' && !location.hostname.includes('127') && !location.hostname.includes('192')) {
;(function () {
const hm = document.createElement('script')
let k = '820b686671af452e8a4e18952ce946d8'
if (location.hostname.includes('vuejs-core')) k = '9578a46b371ba85ee55bc868d6b30692'
hm.src = `//hm.baidu.com/hm.js?${k}`
const s: any = document.querySelectorAll('script')[0]
s.parentNode.insertBefore(hm, s)
})()
;(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
// eslint-disable-next-line prefer-rest-params
;(c[a].q = c[a].q || []).push(arguments)
}
t = l.createElement(r)
t.async = 1
t.src = `//www.clarity.ms/tag/${i}`
y = l.getElementsByTagName(r)[0]
y.parentNode.insertBefore(t, y)
})(globalThis, document, 'clarity', 'script', 'j9de7dmm7n')
}
})
</script>
<template>
<vab-dialog v-model="drawerVisible" append-to-body :title="translate('标签设置')" width="400px">
<el-form ref="form" label-position="top" :model="theme">
<el-form-item v-if="theme.showTabs" :label="translate('标签风格')">
<el-radio-group v-model="theme.tabsBarStyle">
<el-radio-button v-for="item in tabsBarStyleList" :key="item.value" :label="translate(item.label)" :value="item.value" />
</el-radio-group>
</el-form-item>
<el-form-item :label="translate('标签图标')">
<el-switch v-model="theme.showTabsIcon" />
</el-form-item>
<el-form-item :label="translate('持久化标签')">
<el-switch v-model="persistenceTab" @change="handlePersistenceTab" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="handleSaveTheme">
{{ translate('保存') }}
</el-button>
</template>
</vab-dialog>
</template>
<script lang="ts" setup>
import { translate } from '/@/i18n'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabTabsSetting',
})
const settingsStore = useSettingsStore()
const { theme, persistenceTab } = storeToRefs(settingsStore)
const { saveTheme, updateCaughtTabs } = settingsStore
const drawerVisible = ref<boolean>(false)
const tabsBarStyleList = ref<any>([
{ label: '卡片', value: 'card' },
{ label: '灵动', value: 'smart' },
{ label: '圆滑', value: 'smooth' },
{ label: '矩形', value: 'rect' },
])
const handleOpenSetting = () => {
drawerVisible.value = true
}
defineExpose({
handleOpenSetting,
})
const handlePersistenceTab = (value: any) => {
updateCaughtTabs(value)
}
const handleSaveTheme = () => {
saveTheme()
drawerVisible.value = false
}
</script>
<template>
<div v-if="theme.showThemeSetting" class="vab-theme-setting">
<el-collapse-transition>
<section v-show="show">
<div v-show="routeName !== 'SeparateLayout'" @click="randomTheme">
<a>
<vab-icon icon="fire-line" />
<p>{{ translate('随机换肤') }}</p>
</a>
</div>
<div v-show="routeName !== 'SeparateLayout'" @click="handleOpenTheme">
<a>
<vab-icon icon="t-shirt-line" />
<p>{{ translate('主题配置') }}</p>
</a>
</div>
<div @click="changeTheme('technology')">
<a>
<vab-icon icon="user-5-line" />
<p>
{{ translate('科技主题') }}
</p>
</a>
</div>
<div @click="changeTheme('plain')">
<a>
<vab-icon icon="computer-line" />
<p>
{{ translate('简洁主题') }}
</p>
</a>
</div>
<div @click="resetTheme">
<a>
<vab-icon icon="arrow-go-back-line" />
<p>
{{ translate('默认主题') }}
</p>
</a>
</div>
<div @click="removeLocalStorage">
<a>
<vab-icon icon="delete-bin-4-line" />
<p>
{{ translate('清理缓存') }}
</p>
</a>
</div>
</section>
</el-collapse-transition>
<div class="vab-buy-box" @click="buy">
<a class="vab-buy">
<vab-icon icon="shopping-cart-2-line" />
<p>{{ translate('购买源码') }}</p>
</a>
</div>
<div class="vab-show-hide-box" @click="toggleShowHide">
<a>
<vab-icon :icon="show ? 'arrow-up-double-line' : 'arrow-down-double-line'" />
<p>
{{ translate(show ? '收起浮窗' : '展开浮窗') }}
</p>
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import { translate } from '/@/i18n'
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabThemeSetting',
})
const settingsStore = useSettingsStore()
const { device, theme } = storeToRefs(settingsStore)
const { saveTheme, updateTheme, setCssVar, updateCaughtTabs } = settingsStore
const show = ref<boolean>(true)
const route = useRoute()
const routeName = ref<any>(route.name)
const handleOpenTheme = () => {
$pub('shop-vite-open-theme')
}
const buy = () => {
window.open('https://vuejs-core.cn/authorization/shop-vite.html')
}
const removeLocalStorage = () => {
localStorage.clear()
updateCaughtTabs(false)
//@ts-ignore
location.reload(true)
}
const resetTheme = () => {
$pub('shop-vite-reset-theme')
}
const changeTheme = (value: string) => {
$pub('shop-vite-change-theme', value)
$pub('shop-vite-save-theme')
}
const toggleShowHide = () => {
show.value = !show.value
}
const shuffle = (val: any, list: any) => list.filter((item: any) => item !== val)[(Math.random() * (list.length - 1)) | 0]
const randomTheme = async () => {
const loading = $baseLoading()
setTimeout(() => {
const themeName = shuffle(theme.value.themeName, ['default', 'plain', 'technology'])
const columnStyle = shuffle(theme.value.columnStyle, ['vertical', 'horizontal', 'card', 'arrow', 'semicircle'])
const tabsBarStyle = shuffle(theme.value.tabsBarStyle, ['card', 'smart', 'smooth', 'rect'])
const showTabsIcon = shuffle(theme.value.showTabsIcon, [true, false])
const layout =
device.value === 'desktop' ? shuffle(theme.value.layout, ['horizontal', 'vertical', 'column', 'comprehensive', 'fall']) : 'vertical'
const _color = shuffle(theme.value.color, [
'#1e90ff',
'#4e88f3',
'#0052d9',
'#3fb884',
'#16baa9',
'#07c160',
'#009688',
'#6954f0',
'#7b40f2',
'#f01414',
])
const isFollow = shuffle(theme.value.isFollow, [true, false])
theme.value.themeName = themeName
theme.value.columnStyle = columnStyle
theme.value.tabsBarStyle = tabsBarStyle
theme.value.showTabsIcon = showTabsIcon
theme.value.layout = layout
if (themeName === 'technology') {
theme.value.color = '#4e88f3'
} else {
theme.value.color = _color
}
if (themeName === 'default') theme.value.isFollow = isFollow
else theme.value.isFollow = false
setCssVar()
updateTheme()
saveTheme()
setTimeout(() => {
loading.close()
$baseMessage('切换成功', 'success', 'hey')
}, 1000)
}, 100)
}
watch(
route,
() => {
routeName.value = route.name
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
.vab-theme-setting {
position: fixed;
top: 50%;
right: 0;
z-index: calc(var(--el-z-index) - 1);
padding: 10px 0 0 0;
margin: 0;
text-align: center;
cursor: pointer;
background: var(--el-color-white);
border-top: 1px solid var(--el-border-color);
border-bottom: 1px solid var(--el-border-color);
border-left: 1px solid var(--el-border-color);
border-top-left-radius: var(--el-border-radius-base);
border-bottom-left-radius: var(--el-border-radius-base);
box-shadow: 0 0 50px 0 rgb(82 63 105 / 15%);
transform: translateY(-50%);
div {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px 10px 10px;
margin: 0;
list-style: none;
&:nth-child(n) {
a {
&:hover {
color: var(--el-color-white);
}
}
}
&:nth-child(1),
&:nth-child(3),
&:nth-child(4) {
a {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
&:hover {
background: var(--el-color-primary);
}
}
}
&:nth-child(2),
&:nth-child(5) {
a {
color: var(--el-color-success);
background: var(--el-color-success-lighter);
&:hover {
background: var(--el-color-success);
}
}
}
&:nth-child(4) {
a {
color: var(--el-color-info);
background: var(--el-color-info-lighter);
&:hover {
background: var(--el-color-info);
}
}
}
&:nth-child(6) {
a {
color: var(--el-color-danger);
background: var(--el-color-danger-lighter);
&:hover {
background: var(--el-color-danger);
}
}
}
a {
display: inline-block;
width: 60px;
height: 60px;
padding-top: 10px;
text-align: center;
background: var(--el-color-white);
border-radius: var(--el-border-radius-base);
p {
padding: 0;
margin: 0;
overflow: hidden;
font-size: var(--el-font-size-extra-small);
line-height: 25px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.vab-buy-box {
a {
color: var(--el-color-warning) !important;
background: var(--el-color-warning-lighter) !important;
&:hover {
color: var(--el-color-white) !important;
background: var(--el-color-warning) !important;
}
}
}
.vab-show-hide-box {
a {
color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
&:hover {
color: var(--el-color-white) !important;
background: var(--el-color-primary) !important;
}
}
}
}
</style>
<template>
<vab-icon icon="t-shirt-line" @click="handleOpenTheme" />
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabTheme',
})
const handleOpenTheme = () => {
$pub('shop-vite-open-theme')
}
</script>
import { createHead } from '@vueuse/head'
// import ElementPlus from 'element-plus'
import 'virtual:svg-icons-register'
import { VabIcon } from 'vsv-icon'
import type { App } from 'vue'
import './styles/vab.scss'
export const setupVab = (app: App<Element>) => {
// app.use(ElementPlus)
app.use(createHead())
app.component('VabIcon', VabIcon)
const Plugins = import.meta.glob('./plugins/*.ts', { eager: true })
Object.getOwnPropertyNames(Plugins).forEach((key) => {
const plugin: any = Plugins[key]
app.use(plugin.default)
})
}
<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,
'is-no-tabs': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
'is-no-tabs': !showTabs,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<script lang="ts" setup>
import { useSettingsStore } from '/@/store/modules/settings'
defineOptions({
name: 'VabLayoutColumn',
})
defineProps({
collapse: {
type: Boolean,
default: false,
},
fixedHeader: {
type: Boolean,
default: true,
},
showTabs: {
type: Boolean,
default: true,
},
})
const settingsStore = useSettingsStore()
const { theme } = storeToRefs(settingsStore)
</script>
<style lang="scss" scoped>
.vab-layout-column {
.vab-main {
&.is-collapse-main {
&.vab-main-horizontal,
&.vab-main-semicircle {
margin-left: calc(var(--el-left-menu-width-min) * 1.4);
:deep() {
.fixed-header {
width: calc(100% - var(--el-left-menu-width-min) * 1.4);
}
}
}
}
}
}
</style>
<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>
<script lang="ts" setup>
defineOptions({
name: 'VabLayoutComprehensive',
})
defineProps({
collapse: {
type: Boolean,
default: false,
},
fixedHeader: {
type: Boolean,
default: true,
},
showTabs: {
type: Boolean,
default: true,
},
device: {
type: String,
default: 'desktop',
},
})
</script>
<template>
<div
class="vab-layout-fall"
:class="{
fixed: fixedHeader,
'no-tabs-bar': !showTabs,
}"
>
<vab-fall-bar />
<div
class="vab-main"
:class="{
'is-collapse-main': collapse,
'is-no-tabs': !showTabs,
}"
>
<div
class="vab-layout-header"
:class="{
'fixed-header': fixedHeader,
'is-no-tabs': !showTabs,
}"
>
<vab-nav />
<vab-tabs v-show="showTabs" />
</div>
<vab-app-main />
</div>
</div>
</template>
<script lang="ts" setup>
defineOptions({
name: 'VabLayoutFall',
})
defineProps({
collapse: {
type: Boolean,
default: false,
},
fixedHeader: {
type: Boolean,
default: true,
},
showTabs: {
type: Boolean,
default: true,
},
})
</script>
<style lang="scss" scoped>
.vab-layout-fall {
.vab-main {
&.is-collapse-main {
&.vab-main-horizontal,
&.vab-main-semicircle {
margin-left: calc(var(--el-left-menu-width-min) * 1.4);
:deep() {
.fixed-header {
width: calc(100% - var(--el-left-menu-width-min) * 1.4);
}
}
}
}
}
}
</style>
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论