mod: 清空该分支
@ -1,13 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
@ -1,5 +0,0 @@
|
||||
# 请勿改动这一项,该项也不可以通过 import.meta.env.NODE_ENV 调用
|
||||
NODE_ENV = development
|
||||
|
||||
# 自定义的环境变量可以修改(命名必须以 VITE_ 开头)
|
||||
VITE_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1'
|
@ -1,5 +0,0 @@
|
||||
# 请勿改动这一项,该项也不可以通过 import.meta.env.NODE_ENV 调用
|
||||
NODE_ENV = production
|
||||
|
||||
# 自定义的环境变量可以修改(命名必须以 VITE_ 开头)
|
||||
VITE_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1'
|
@ -1,5 +0,0 @@
|
||||
# 请勿改动这一项,该项也不可以通过 import.meta.env.NODE_ENV 调用
|
||||
NODE_ENV = production
|
||||
|
||||
# 自定义的环境变量可以修改(命名必须以 VITE_ 开头)
|
||||
VITE_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1'
|
@ -1,7 +0,0 @@
|
||||
# eslint 会忽略的文件
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
81
.eslintrc.js
@ -1,81 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true
|
||||
},
|
||||
globals: {
|
||||
// script setup
|
||||
defineProps: "readonly",
|
||||
defineEmits: "readonly",
|
||||
defineExpose: "readonly",
|
||||
withDefaults: "readonly"
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended",
|
||||
"@vue/prettier",
|
||||
"@vue/eslint-config-typescript"
|
||||
],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
jsxPragma: "React",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// ts
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-debugger": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
// vue
|
||||
"vue/no-v-html": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/require-explicit-emits": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/html-self-closing": [
|
||||
"error",
|
||||
{
|
||||
html: {
|
||||
void: "always",
|
||||
normal: "always",
|
||||
component: "always"
|
||||
},
|
||||
svg: "always",
|
||||
math: "always"
|
||||
}
|
||||
],
|
||||
// prettier
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
endOfLine: "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
32
.gitignore
vendored
@ -1,32 +0,0 @@
|
||||
# git 会忽略的文件
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# local env files
|
||||
*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Use the PNPM
|
||||
package-lock.json
|
||||
yarn.lock
|
@ -1,8 +0,0 @@
|
||||
# prettier 会忽略的文件
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.d.ts
|
9
.vscode/extensions.json
vendored
@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"johnsoncodehk.vscode-typescript-vue-plugin",
|
||||
"johnsoncodehk.volar"
|
||||
]
|
||||
}
|
21
LICENSE
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 pany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
97
README.md
@ -1,97 +0,0 @@
|
||||
## ⚡️ 简介
|
||||
|
||||
一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element-Plus、Pinia 和 Vite.
|
||||
|
||||
模板代码是从 [v3-admin v3.1.3](https://github.com/un-pany/v3-admin) 迁移而来,只是脚手架从 vue-cli 5.x 切换到了 vite,并作了一些繁琐的适配.
|
||||
|
||||
更推荐大家使用该 vite 版本!以后的重心也会从 [v3-admin](https://github.com/un-pany/v3-admin) 偏向本仓库.
|
||||
|
||||
## 📚 文档
|
||||
|
||||
[简体中文](https://juejin.cn/post/7089377403717287972)
|
||||
|
||||
## 国内仓库
|
||||
|
||||
[Gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||
|
||||
## 预览
|
||||
|
||||
| 位置 | 账号 | 链接 |
|
||||
| --- | --- | --- |
|
||||
| github-pages | admin或editor | [链接](https://un-pany.github.io/v3-admin-vite) |
|
||||
|
||||
## 🚀 开发
|
||||
|
||||
```bash
|
||||
# 配置
|
||||
1. 安装 .vscode 中推荐的插件
|
||||
3. node 版本 16+
|
||||
4. pnpm 版本 6.x
|
||||
|
||||
# 克隆项目
|
||||
git clone https://github.com/un-pany/v3-admin-vite.git
|
||||
|
||||
# 进入项目目录
|
||||
cd v3-admin-vite
|
||||
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
|
||||
# 启动服务
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## ✔️ 预览
|
||||
|
||||
```bash
|
||||
# 预览预发布环境
|
||||
pnpm preview:stage
|
||||
|
||||
# 预览正式环境
|
||||
pnpm preview:prod
|
||||
```
|
||||
|
||||
## 📦️ 多环境打包
|
||||
|
||||
```bash
|
||||
# 构建预发布环境
|
||||
pnpm build:stage
|
||||
|
||||
# 构建正式环境
|
||||
pnpm build:prod
|
||||
```
|
||||
|
||||
## 🔧 代码格式检查
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
- `mod` 不确定分类的修改
|
||||
|
||||
## 交流(吹水)群
|
||||
|
||||
QQ群:1014374415
|
||||
|
||||

|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](https://github.com/un-pany/v3-admin-vite/blob/main/LICENSE)
|
||||
|
||||
Copyright (c) 2022 pany
|
16
index.html
@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/app-loading.css" />
|
||||
<title>v3-admin-vite</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="app-loading"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
83
package.json
@ -1,83 +0,0 @@
|
||||
{
|
||||
"name": "v3-admin-vite",
|
||||
"version": "3.1.3",
|
||||
"description": "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element-Plus、Pinia 和 Vite",
|
||||
"author": {
|
||||
"name": "pany",
|
||||
"email": "939630029@qq.com",
|
||||
"url": "https://github.com/pany-ang"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/un-pany/v3-admin-vite.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:stage": "vue-tsc --noEmit && vite build --mode staging",
|
||||
"build:prod": "vue-tsc --noEmit && vite build",
|
||||
"preview:stage": "pnpm build:stage && vite preview",
|
||||
"preview:prod": "pnpm build:prod && vite preview",
|
||||
"lint:eslint": "eslint \"src/**/*.{vue,ts,tsx}\" --fix",
|
||||
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"axios": "^0.26.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"element-plus": "^2.1.10",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pinia": "^2.0.13",
|
||||
"screenfull": "^6.0.1",
|
||||
"vue": "^3.2.33",
|
||||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^17.0.25",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||
"@typescript-eslint/parser": "^5.20.0",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.6.0",
|
||||
"lint-staged": "^12.4.0",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.50.1",
|
||||
"typescript": "^4.6.3",
|
||||
"unplugin-auto-import": "^0.7.1",
|
||||
"unplugin-vue-components": "^0.19.3",
|
||||
"vite": "^2.9.5",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-eslint-parser": "^8.3.0",
|
||||
"vue-tsc": "^0.34.7"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,vue,ts,tsx}": [
|
||||
"pnpm lint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"element-plus",
|
||||
"vue3",
|
||||
"ts",
|
||||
"admin",
|
||||
"typescript"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
3379
pnpm-lock.yaml
generated
@ -1,21 +0,0 @@
|
||||
/** 配置项文档:https://prettier.io/docs/en/configuration.html */
|
||||
module.exports = {
|
||||
/** 每一行的宽度 */
|
||||
printWidth: 120,
|
||||
/** tab 健的空格数 */
|
||||
tabWidth: 2,
|
||||
/** 在对象中的括号之间用空格来间隔 */
|
||||
bracketSpacing: true,
|
||||
/** 箭头函数的参数无论有几个,都要括号包裹 */
|
||||
arrowParens: "always",
|
||||
/** 换行符的使用 */
|
||||
endOfLine: "auto",
|
||||
/** 是否采用单引号 */
|
||||
singleQuote: false,
|
||||
/** 对象或者数组的最后一个元素后面不要加逗号 */
|
||||
trailingComma: "none",
|
||||
/** 不加分号 */
|
||||
semi: false,
|
||||
/** 不使用 tab 格式化 */
|
||||
useTabs: false
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
#app-loading,
|
||||
#app-loading:before,
|
||||
#app-loading:after {
|
||||
border-radius: 50%;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation: loadingAnimation 1.8s infinite ease-in-out;
|
||||
animation: loadingAnimation 1.8s infinite ease-in-out;
|
||||
}
|
||||
|
||||
#app-loading {
|
||||
color: #409eff;
|
||||
font-size: 10px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
top: 0;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
#app-loading:before,
|
||||
#app-loading:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#app-loading:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
#app-loading:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
|
||||
@-webkit-keyframes loadingAnimation {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingAnimation {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 66 KiB |
14
src/App.vue
@ -1,14 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { ElConfigProvider } from "element-plus"
|
||||
import zhCn from "element-plus/lib/locale/lang/zh-cn"
|
||||
|
||||
useAppStore().initTheme() // 初始化 theme
|
||||
const locale = zhCn // element-plus 设置为中文
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="locale">
|
||||
<router-view />
|
||||
</ElConfigProvider>
|
||||
</template>
|
@ -1,22 +0,0 @@
|
||||
import { request } from "@/utils/service"
|
||||
|
||||
interface IUserRequestData {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 登录,返回 token */
|
||||
export function accountLogin(data: IUserRequestData) {
|
||||
return request({
|
||||
url: "users/login",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
/** 获取用户详情 */
|
||||
export function userInfoRequest() {
|
||||
return request({
|
||||
url: "users/info",
|
||||
method: "post"
|
||||
})
|
||||
}
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 317 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 352 KiB |
Before Width: | Height: | Size: 379 KiB |
Before Width: | Height: | Size: 370 KiB |
Before Width: | Height: | Size: 3.4 KiB |
@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1646185144620" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3301" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M899.7 116.1 645 116.1 296.8 440.7 296.8 116.1 65 116.1 65 208.8 157.7 208.8 157.7 904.4Z" p-id="3302" fill="#409EFF"></path><path d="M603.9 626.7 603.9 517c23.7-13.9 49.9-24.6 78.5-31.9 28.6-7.3 58.1-10.9 88.7-10.9 53 0 93.5 11.4 121.5 34.1 28 22.7 42 55.4 42 98.1 0 29.4-7.1 53.1-21.3 71.1-14.2 18.1-35.9 31-65.1 38.9 30.1 4.2 52.7 16.1 67.7 35.5 15.1 19.4 22.6 46.3 22.6 80.5 0 47.5-14.5 83.4-43.5 107.5C866 964 822.9 976 765.8 976c-28.1 0-56.5-3.8-85-11.4-28.6-7.6-56.1-18.6-82.4-33L598.4 816.9l114 0 0 3.8c0 19 4.6 33.5 13.9 43.2 9.2 9.9 22.8 14.8 40.9 14.8 16.9 0 29.8-4.9 38.7-14.8 8.9-9.8 13.4-24 13.4-42.6 0-21.3-5.1-36.5-15.3-45.5-10.2-9-28.3-13.4-54.2-13.4l-63 0 0-81.8 66.9 0c21.6 0 38.1-4.8 49.6-14.4 11.5-9.6 17.3-23.2 17.3-40.7 0-17.3-5-30.7-15.2-40.3-10.1-9.5-24.2-14.3-42.2-14.3-15.6 0-27.6 4.7-35.9 14.1-8.4 9.4-13.1 23.3-14.2 41.7L603.9 626.7z" p-id="3303" fill="#409EFF"></path></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,23 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from "element-plus"
|
||||
import { FullScreen } from "@element-plus/icons-vue"
|
||||
import screenfull from "screenfull"
|
||||
|
||||
const click = () => {
|
||||
if (!screenfull.isEnabled) {
|
||||
ElMessage.warning("您的浏览器无法工作")
|
||||
return
|
||||
}
|
||||
screenfull.toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="click">
|
||||
<el-tooltip effect="dark" content="全屏" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<FullScreen />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
@ -1,31 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon"
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="svg-icon" aria-hidden="true">
|
||||
<use :href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -1,39 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { MagicStick } from "@element-plus/icons-vue"
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const themeList = computed(() => {
|
||||
return appStore.themeList
|
||||
})
|
||||
const activeThemeName = computed(() => {
|
||||
return appStore.activeThemeName
|
||||
})
|
||||
const handleSetTheme = (name: string) => {
|
||||
appStore.setTheme(name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleSetTheme">
|
||||
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<MagicStick />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(theme, index) in themeList"
|
||||
:key="index"
|
||||
:disabled="activeThemeName === theme.name"
|
||||
:command="theme.name"
|
||||
>
|
||||
<span>{{ theme.title }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
@ -1,21 +0,0 @@
|
||||
/** 动态路由配置 */
|
||||
interface IAsyncRouteSettings {
|
||||
/**
|
||||
* 是否开启动态路由功能?
|
||||
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
|
||||
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 open: false
|
||||
*/
|
||||
open: boolean
|
||||
/** 当动态路由功能关闭时:
|
||||
* 1. 应该将所有路由都写到常驻路由里面(表明所有登陆的用户能访问的页面都是一样的)
|
||||
* 2. 系统自动给当前登录用户赋值一个默认的角色(默认为 admin,拥有所有页面权限)
|
||||
*/
|
||||
defaultRoles: Array<string>
|
||||
}
|
||||
|
||||
const asyncRouteSettings: IAsyncRouteSettings = {
|
||||
open: true,
|
||||
defaultRoles: ["admin"]
|
||||
}
|
||||
|
||||
export default asyncRouteSettings
|
@ -1,26 +0,0 @@
|
||||
/** 布局配置 */
|
||||
interface ILayoutSettings {
|
||||
/** 控制 settings panel 显示 */
|
||||
showSettings: boolean
|
||||
/** 控制 tagsview 显示 */
|
||||
showTagsView: boolean
|
||||
/** 控制 siderbar logo 显示 */
|
||||
showSidebarLogo: boolean
|
||||
/** 如果为真,将固定 header */
|
||||
fixedHeader: boolean
|
||||
/** 控制 换肤按钮 显示 */
|
||||
showThemeSwitch: boolean
|
||||
/** 控制 全屏按钮 显示 */
|
||||
showScreenfull: boolean
|
||||
}
|
||||
|
||||
const layoutSettings: ILayoutSettings = {
|
||||
showSettings: true,
|
||||
showTagsView: true,
|
||||
fixedHeader: false,
|
||||
showSidebarLogo: true,
|
||||
showThemeSwitch: true,
|
||||
showScreenfull: true
|
||||
}
|
||||
|
||||
export default layoutSettings
|
@ -1,13 +0,0 @@
|
||||
/** 注册的主题 */
|
||||
const themeList = [
|
||||
{
|
||||
title: "默认",
|
||||
name: "normal"
|
||||
},
|
||||
{
|
||||
title: "黑暗",
|
||||
name: "dark"
|
||||
}
|
||||
]
|
||||
|
||||
export default themeList
|
@ -1,4 +0,0 @@
|
||||
/** 免登录白名单 */
|
||||
const whiteList = ["/login"]
|
||||
|
||||
export { whiteList }
|
@ -1,7 +0,0 @@
|
||||
class Keys {
|
||||
static sidebarStatus = "v3-admin-vite-sidebar-status-key"
|
||||
static token = "v3-admin-vite-token-key"
|
||||
static activeThemeName = "v3-admin-vite-active-theme-name-key"
|
||||
}
|
||||
|
||||
export default Keys
|
@ -1 +0,0 @@
|
||||
export * from "./permission"
|
@ -1,21 +0,0 @@
|
||||
import { Directive } from "vue"
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
|
||||
/** 权限指令 */
|
||||
export const permission: Directive = {
|
||||
mounted(el, binding) {
|
||||
const { value } = binding
|
||||
const roles = useUserStoreHook().roles
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissionRoles = value
|
||||
const hasPermission = roles.some((role: any) => {
|
||||
return permissionRoles.includes(role)
|
||||
})
|
||||
if (!hasPermission) {
|
||||
el.style.display = "none"
|
||||
}
|
||||
} else {
|
||||
throw new Error("need roles! Like v-permission=\"['admin','editor']\"")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { createApp } from "vue"
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue" // svg component
|
||||
import "virtual:svg-icons-register"
|
||||
|
||||
export default (app: ReturnType<typeof createApp>) => {
|
||||
app.component("SvgIcon", SvgIcon)
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1 +0,0 @@
|
||||
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1 +0,0 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>
|
Before Width: | Height: | Size: 444 B |
@ -1,47 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const key = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<!-- <keep-alive> -->
|
||||
<component :is="Component" :key="key" />
|
||||
<!-- </keep-alive> -->
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50 = navbar height */
|
||||
min-height: calc(100vh - 50px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,83 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, reactive, watch } from "vue"
|
||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router"
|
||||
import { compile } from "path-to-regexp"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const pathCompile = (path: string) => {
|
||||
const { params } = route
|
||||
const toPath = compile(path)
|
||||
return toPath(params)
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
breadcrumbs: [] as Array<RouteLocationMatched>,
|
||||
getBreadcrumb: () => {
|
||||
const matched = route.matched.filter((item) => item.meta && item.meta.title)
|
||||
state.breadcrumbs = matched.filter((item) => {
|
||||
return item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||
})
|
||||
},
|
||||
handleLink(item: any) {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
router.push(redirect).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
return
|
||||
}
|
||||
router.push(pathCompile(path)).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(path) => {
|
||||
if (path.startsWith("/redirect/")) {
|
||||
return
|
||||
}
|
||||
state.getBreadcrumb()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeMount(() => {
|
||||
state.getBreadcrumb()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in state.breadcrumbs" :key="item.path">
|
||||
<span v-if="item.redirect === 'noRedirect' || index === state.breadcrumbs.length - 1" class="no-redirect">{{
|
||||
item.meta.title
|
||||
}}</span>
|
||||
<a v-else @click.prevent="state.handleLink(item)">
|
||||
{{ item.meta.title }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,31 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { Expand, Fold } from "@element-plus/icons-vue"
|
||||
|
||||
defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(["toggle-click"])
|
||||
|
||||
const toggleClick = () => {
|
||||
emit("toggle-click")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="toggleClick">
|
||||
<el-icon :size="20" class="icon">
|
||||
<fold v-if="isActive" />
|
||||
<expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
@ -1,115 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { UserFilled } from "@element-plus/icons-vue"
|
||||
import BreadCrumb from "../BreadCrumb/index.vue"
|
||||
import Hamburger from "../Hamburger/index.vue"
|
||||
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
|
||||
import Screenfull from "@/components/Screenfull/index.vue"
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return appStore.sidebar
|
||||
})
|
||||
const showThemeSwitch = computed(() => {
|
||||
return settingsStore.showThemeSwitch
|
||||
})
|
||||
const showScreenfull = computed(() => {
|
||||
return settingsStore.showScreenfull
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
toggleSideBar: () => {
|
||||
appStore.toggleSidebar(false)
|
||||
},
|
||||
logout: () => {
|
||||
userStore.logout()
|
||||
router.push("/login").catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<Hamburger :is-active="sidebar.opened" class="hamburger" @toggle-click="state.toggleSideBar" />
|
||||
<BreadCrumb class="breadcrumb" />
|
||||
<div class="right-menu">
|
||||
<Screenfull v-if="showScreenfull" class="right-menu-item" />
|
||||
<ThemeSwitch v-if="showThemeSwitch" class="right-menu-item" />
|
||||
<el-dropdown class="right-menu-item">
|
||||
<el-avatar :icon="UserFilled" :size="34" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<a target="_blank" href="#">
|
||||
<el-dropdown-item>V3-Admin-Vite 中文文档</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="#">
|
||||
<el-dropdown-item>V3-Admin-Vite English Docs</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/un-pany/v3-admin-vite">
|
||||
<el-dropdown-item>V3-Admin-Vite GitHub</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="#">
|
||||
<el-dropdown-item>V3-Admin-Vite Gitee</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://juejin.cn/post/6963876125428678693">
|
||||
<el-dropdown-item divided>V3-Admin 中文文档</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/un-pany/v3-admin/blob/master/README.en.md">
|
||||
<el-dropdown-item>V3-Admin English Docs</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/un-pany/v3-admin">
|
||||
<el-dropdown-item>V3-Admin GitHub</el-dropdown-item>
|
||||
</a>
|
||||
<a target="_blank" href="https://gitee.com/un-pany/v3-admin">
|
||||
<el-dropdown-item>V3-Admin Gitee</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item divided @click="state.logout">
|
||||
<span style="display: block">退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
.hamburger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
float: left;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.breadcrumb {
|
||||
float: left;
|
||||
}
|
||||
.right-menu {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #5a5e66;
|
||||
.right-menu-item {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,44 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { Setting } from "@element-plus/icons-vue"
|
||||
|
||||
defineProps({
|
||||
buttonTop: {
|
||||
type: Number,
|
||||
default: 250
|
||||
}
|
||||
})
|
||||
|
||||
const show = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="handle-button" :style="{ top: buttonTop + 'px' }" @click="show = true">
|
||||
<el-icon :size="24">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-drawer v-model="show" size="300px" :with-header="false">
|
||||
<slot />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.handle-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: #152d3d;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
border-radius: 6px 0 0 6px !important;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -1,111 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const state = reactive({
|
||||
fixedHeader: settingsStore.fixedHeader,
|
||||
showTagsView: settingsStore.showTagsView,
|
||||
showSidebarLogo: settingsStore.showSidebarLogo,
|
||||
showThemeSwitch: settingsStore.showThemeSwitch,
|
||||
showScreenfull: settingsStore.showScreenfull
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.fixedHeader,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "fixedHeader",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showTagsView,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showTagsView",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showSidebarLogo,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showSidebarLogo",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showThemeSwitch,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showThemeSwitch",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showScreenfull,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showScreenfull",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drawer-container">
|
||||
<div>
|
||||
<h3 class="drawer-title">系统布局配置</h3>
|
||||
<div class="drawer-item">
|
||||
<span>显示 Tags-View</span>
|
||||
<el-switch v-model="state.showTagsView" class="drawer-switch" />
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>显示侧边栏 Logo</span>
|
||||
<el-switch v-model="state.showSidebarLogo" class="drawer-switch" />
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>固定 Header</span>
|
||||
<el-switch v-model="state.fixedHeader" class="drawer-switch" />
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>显示换肤按钮</span>
|
||||
<el-switch v-model="state.showThemeSwitch" class="drawer-switch" />
|
||||
</div>
|
||||
<div class="drawer-item">
|
||||
<span>显示全屏按钮</span>
|
||||
<el-switch v-model="state.showScreenfull" class="drawer-switch" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer-container {
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
.drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.drawer-item {
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.drawer-switch {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,116 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, PropType } from "vue"
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import path from "path-browserify"
|
||||
import SidebarItemLink from "./SidebarItemLink.vue"
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<RouteRecordRaw>,
|
||||
required: true
|
||||
},
|
||||
isCollapse: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
isFirstLevel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const alwaysShowRootMenu = computed(() => {
|
||||
return !!(props.item.meta && props.item.meta.alwaysShow)
|
||||
})
|
||||
const showingChildNumber = computed(() => {
|
||||
if (props.item.children) {
|
||||
const showingChildren = props.item.children.filter((item) => {
|
||||
return !(item.meta && item.meta.hidden)
|
||||
})
|
||||
return showingChildren.length
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const theOnlyOneChild = computed(() => {
|
||||
if (showingChildNumber.value > 1) {
|
||||
return null
|
||||
}
|
||||
if (props.item.children) {
|
||||
for (const child of props.item.children) {
|
||||
if (!child.meta || !child.meta.hidden) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no children, return itself with path removed,
|
||||
// because this.basePath already contains item's path information
|
||||
return { ...props.item, path: "" }
|
||||
})
|
||||
|
||||
const resolvePath = (routePath: string) => {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath
|
||||
}
|
||||
return path.resolve(props.basePath, routePath)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden" :class="{ 'simple-mode': isCollapse, 'first-level': isFirstLevel }">
|
||||
<template v-if="!alwaysShowRootMenu && theOnlyOneChild && !theOnlyOneChild.children">
|
||||
<SidebarItemLink v-if="theOnlyOneChild.meta" :to="resolvePath(theOnlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(theOnlyOneChild.path)">
|
||||
<svg-icon v-if="theOnlyOneChild.meta.icon" :name="theOnlyOneChild.meta.icon" />
|
||||
<template v-if="theOnlyOneChild.meta.title" #title>
|
||||
{{ theOnlyOneChild.meta.title }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</SidebarItemLink>
|
||||
</template>
|
||||
<el-sub-menu v-else :index="resolvePath(item.path)" popper-append-to-body>
|
||||
<template #title>
|
||||
<svg-icon v-if="item.meta && item.meta.icon" :name="item.meta.icon" />
|
||||
<span v-if="item.meta && item.meta.title">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
<template v-if="item.children">
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:item="child"
|
||||
:is-collapse="isCollapse"
|
||||
:is-first-level="false"
|
||||
:base-path="resolvePath(child.path)"
|
||||
/>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
margin-right: 20px;
|
||||
min-width: 1em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.simple-mode {
|
||||
&.first-level {
|
||||
::v-deep(.el-sub-menu) {
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
span {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,28 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const push = () => {
|
||||
router.push(props.to).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
|
||||
<slot />
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
@ -1,76 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img src="@/assets/layout/logo.png" class="sidebar-logo" />
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img src="@/assets/layout/logo-text-1.png" class="sidebar-logo-text" />
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active,
|
||||
.sidebarLogoFade-leave-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter-from,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 84px;
|
||||
line-height: 84px;
|
||||
background: #0c202b;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
.sidebar-logo {
|
||||
display: none;
|
||||
}
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
& .sidebar-logo-text {
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
font-size: 16px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
&.collapse {
|
||||
.sidebar-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.sidebar-logo-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,143 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import SidebarItem from "./SidebarItem.vue"
|
||||
import SidebarLogo from "./SidebarLogo.vue"
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return useAppStore().sidebar
|
||||
})
|
||||
const routes = computed(() => {
|
||||
return usePermissionStore().routes
|
||||
})
|
||||
const showLogo = computed(() => {
|
||||
return useSettingsStore().showSidebarLogo
|
||||
})
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta !== null || meta !== undefined) {
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
}
|
||||
return path
|
||||
})
|
||||
const isCollapse = computed(() => {
|
||||
return !sidebar.value.opened
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'has-logo': showLogo }">
|
||||
<SidebarLogo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:unique-opened="true"
|
||||
:default-active="activeMenu"
|
||||
background-color="#152d3d"
|
||||
text-color="#C0C4CC"
|
||||
active-text-color="#fff"
|
||||
mode="vertical"
|
||||
>
|
||||
<SidebarItem
|
||||
v-for="routeItem in routes"
|
||||
:key="routeItem.path"
|
||||
:item="routeItem"
|
||||
:base-path="routeItem.path"
|
||||
:is-collapse="isCollapse"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-container {
|
||||
// 重置当前页面的 element-plus css, ,注意,虽然没有加 scoped 标识,但是被该页面的 sidebar-container 类名包裹,所以不会影响其他页面
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
||||
}
|
||||
.scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
.el-scrollbar__view {
|
||||
height: 100%;
|
||||
}
|
||||
.el-scrollbar__bar {
|
||||
&.is-vertical {
|
||||
right: 0;
|
||||
}
|
||||
&.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin tip-line {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-logo {
|
||||
.el-scrollbar {
|
||||
// 84px 是 logo 区域的 height
|
||||
height: calc(100% - 84px);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-menu-item),
|
||||
::v-deep(.el-sub-menu__title),
|
||||
::v-deep(.el-sub-menu .el-menu-item) {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
&:hover {
|
||||
background-color: #ffffff10;
|
||||
}
|
||||
display: block;
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
@include tip-line;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
::v-deep(.el-sub-menu) {
|
||||
&.is-active {
|
||||
.el-sub-menu__title {
|
||||
color: #fff !important;
|
||||
@include tip-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<el-scrollbar :vertical="false" class="scroll-container">
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
// 超出窗口长度时,显示滚动条
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1,286 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, getCurrentInstance, nextTick, onBeforeMount, reactive, watch } from "vue"
|
||||
import { RouteRecordRaw, useRoute, useRouter } from "vue-router"
|
||||
import { useTagsViewStore, ITagView } from "@/store/modules/tags-view"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { Close } from "@element-plus/icons-vue"
|
||||
import path from "path-browserify"
|
||||
import ScrollPane from "./ScrollPane.vue"
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const toLastView = (visitedViews: ITagView[], view: ITagView) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView !== undefined && latestView.fullPath !== undefined) {
|
||||
router.push(latestView.fullPath).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
} else {
|
||||
// 如果没有 tags-view,请默认重定向到主页,如果你需要,可以自行调整它
|
||||
if (view.name === "Dashboard") {
|
||||
// 重新加载主页
|
||||
router.push({ path: "/redirect" + view.fullPath }).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
} else {
|
||||
router.push("/").catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {} as ITagView,
|
||||
affixTags: [] as ITagView[],
|
||||
isActive: (tagView: ITagView) => {
|
||||
return tagView.path === route.path
|
||||
},
|
||||
isAffix: (tag: ITagView) => {
|
||||
return tag.meta && tag.meta.affix
|
||||
},
|
||||
refreshSelectedTag: (view: ITagView) => {
|
||||
const { fullPath } = view
|
||||
nextTick(() => {
|
||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
closeSelectedTag: (view: ITagView) => {
|
||||
tagsViewStore.delVisitedView(view)
|
||||
if (state.isActive(view)) {
|
||||
toLastView(tagsViewStore.visitedViews, view)
|
||||
}
|
||||
},
|
||||
closeOthersTags: () => {
|
||||
if (state.selectedTag.fullPath !== route.path && state.selectedTag.fullPath !== undefined) {
|
||||
router.push(state.selectedTag.fullPath).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
tagsViewStore.delOthersVisitedViews(state.selectedTag as ITagView)
|
||||
},
|
||||
closeAllTags: (view: ITagView) => {
|
||||
tagsViewStore.delAllVisitedViews()
|
||||
if (state.affixTags.some((tag) => tag.path === route.path)) {
|
||||
return
|
||||
}
|
||||
toLastView(tagsViewStore.visitedViews, view)
|
||||
},
|
||||
openMenu: (tag: ITagView, e: MouseEvent) => {
|
||||
const menuMinWidth = 105
|
||||
const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = instance!.proxy!.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
||||
if (left > maxLeft) {
|
||||
state.left = maxLeft
|
||||
} else {
|
||||
state.left = left
|
||||
}
|
||||
state.top = e.clientY
|
||||
state.visible = true
|
||||
state.selectedTag = tag
|
||||
},
|
||||
closeMenu: () => {
|
||||
state.visible = false
|
||||
}
|
||||
})
|
||||
|
||||
const visitedViews = computed(() => {
|
||||
return tagsViewStore.visitedViews
|
||||
})
|
||||
const routes = computed(() => permissionStore.routes)
|
||||
|
||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||
let tags: ITagView[] = []
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path)
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta }
|
||||
})
|
||||
}
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path)
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags)
|
||||
}
|
||||
}
|
||||
})
|
||||
return tags
|
||||
}
|
||||
|
||||
const initTags = () => {
|
||||
state.affixTags = filterAffixTags(routes.value)
|
||||
for (const tag of state.affixTags) {
|
||||
// 必须含有 name 属性
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag as ITagView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addTags = () => {
|
||||
if (route.name) {
|
||||
tagsViewStore.addVisitedView(route)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const moveToCurrentTag = () => {
|
||||
const tags = instance?.refs.tag as any[]
|
||||
if (tags === null || tags === undefined || !Array.isArray(tags)) {
|
||||
return
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if ((tag.to as ITagView).path === route.path) {
|
||||
// When query is different then update
|
||||
if ((tag.to as ITagView).fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.visible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener("click", state.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener("click", state.closeMenu)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// life cricle
|
||||
onBeforeMount(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<ScrollPane class="tags-view-wrapper">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="state.isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
class="tags-view-item"
|
||||
@click.middle="!state.isAffix(tag) ? state.closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="state.openMenu(tag, $event)"
|
||||
>
|
||||
{{ tag.meta?.title }}
|
||||
<el-icon v-if="!state.isAffix(tag)" :size="12" @click.prevent.stop="state.closeSelectedTag(tag)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</ScrollPane>
|
||||
<ul v-show="state.visible" :style="{ left: state.left + 'px', top: state.top + 'px' }" class="contextmenu">
|
||||
<li @click="state.refreshSelectedTag(state.selectedTag)">刷新</li>
|
||||
<li v-if="!state.isAffix(state.selectedTag)" @click="state.closeSelectedTag(state.selectedTag)">关闭</li>
|
||||
<li @click="state.closeOthersTags">关闭其它</li>
|
||||
<li @click="state.closeAllTags(state.selectedTag)">关闭所有</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
box-shadow: 0 1px 3px 0 #00000010, 0 0 3px 0 #00000010;
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
border: 1px solid #d8dce5;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
&.active {
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border-color: #409eff;
|
||||
&::before {
|
||||
content: "";
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.el-icon {
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
&:hover {
|
||||
background-color: #00000030;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 #00000030;
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +0,0 @@
|
||||
export { default as AppMain } from "./AppMain.vue"
|
||||
export { default as NavigationBar } from "./NavigationBar/index.vue"
|
||||
export { default as Settings } from "./Settings/index.vue"
|
||||
export { default as Sidebar } from "./Sidebar/index.vue"
|
||||
export { default as TagsView } from "./TagsView/index.vue"
|
||||
export { default as RightPanel } from "./RightPanel/index.vue"
|
@ -1,157 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive } from "vue"
|
||||
import { useAppStore, DeviceType } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { AppMain, NavigationBar, Settings, Sidebar, TagsView, RightPanel } from "./components"
|
||||
import useResize from "./useResize"
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { sidebar, device, addEventListenerOnResize, resizeMounted, removeEventListenerResize, watchRouter } = useResize()
|
||||
|
||||
const state = reactive({
|
||||
handleClickOutside: () => {
|
||||
appStore.closeSidebar(false)
|
||||
}
|
||||
})
|
||||
const classObj = computed(() => {
|
||||
return {
|
||||
hideSidebar: !sidebar.value.opened,
|
||||
openSidebar: sidebar.value.opened,
|
||||
withoutAnimation: sidebar.value.withoutAnimation,
|
||||
mobile: device.value === DeviceType.Mobile
|
||||
}
|
||||
})
|
||||
const showSettings = computed(() => {
|
||||
return settingsStore.showSettings
|
||||
})
|
||||
const showTagsView = computed(() => {
|
||||
return settingsStore.showTagsView
|
||||
})
|
||||
const fixedHeader = computed(() => {
|
||||
return settingsStore.fixedHeader
|
||||
})
|
||||
|
||||
watchRouter()
|
||||
onBeforeMount(() => {
|
||||
addEventListenerOnResize()
|
||||
})
|
||||
onMounted(() => {
|
||||
resizeMounted()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListenerResize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="classObj.mobile && sidebar.opened" class="drawer-bg" @click="state.handleClickOutside" />
|
||||
<Sidebar class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<NavigationBar />
|
||||
<TagsView v-if="showTagsView" />
|
||||
</div>
|
||||
<AppMain />
|
||||
<RightPanel v-if="showSettings">
|
||||
<Settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/mixins.scss";
|
||||
$sideBarWidth: 220px;
|
||||
|
||||
.app-wrapper {
|
||||
@include clearfix;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
transition: margin-left 0.28s;
|
||||
margin-left: $sideBarWidth;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: width 0.28s;
|
||||
width: $sideBarWidth !important;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
font-size: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.main-container {
|
||||
margin-left: 54px;
|
||||
}
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
}
|
||||
.fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
}
|
||||
|
||||
// for mobile response 适配移动端
|
||||
.mobile {
|
||||
.main-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
.sidebar-container {
|
||||
transition: transform 0.28s;
|
||||
width: $sideBarWidth !important;
|
||||
}
|
||||
&.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
&.hideSidebar {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.withoutAnimation {
|
||||
.main-container,
|
||||
.sidebar-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,67 +0,0 @@
|
||||
import { computed, watch } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useAppStore, DeviceType } from "@/store/modules/app"
|
||||
|
||||
/** 参考 Bootstrap 的响应式设计 width = 992 */
|
||||
const WIDTH = 992
|
||||
|
||||
/** 根据大小变化重新布局 */
|
||||
export default () => {
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const device = computed(() => {
|
||||
return appStore.device
|
||||
})
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return appStore.sidebar
|
||||
})
|
||||
|
||||
const watchRouter = watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
if (appStore.device === DeviceType.Mobile && appStore.sidebar.opened) {
|
||||
appStore.closeSidebar(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const isMobile = () => {
|
||||
const rect = document.body.getBoundingClientRect()
|
||||
return rect.width - 1 < WIDTH
|
||||
}
|
||||
|
||||
const resizeMounted = () => {
|
||||
if (isMobile()) {
|
||||
appStore.toggleDevice(DeviceType.Mobile)
|
||||
appStore.closeSidebar(true)
|
||||
}
|
||||
}
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (!document.hidden) {
|
||||
appStore.toggleDevice(isMobile() ? DeviceType.Mobile : DeviceType.Desktop)
|
||||
if (isMobile()) {
|
||||
appStore.closeSidebar(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addEventListenerOnResize = () => {
|
||||
window.addEventListener("resize", resizeHandler)
|
||||
}
|
||||
|
||||
const removeEventListenerResize = () => {
|
||||
window.removeEventListener("resize", resizeHandler)
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
resizeMounted,
|
||||
addEventListenerOnResize,
|
||||
removeEventListenerResize,
|
||||
watchRouter
|
||||
}
|
||||
}
|
19
src/main.ts
@ -1,19 +0,0 @@
|
||||
import { createApp, Directive } from "vue"
|
||||
import router from "./router"
|
||||
import "@/router/permission"
|
||||
import store from "./store"
|
||||
import App from "./App.vue"
|
||||
import * as directives from "@/directives"
|
||||
import loadSvg from "@/icons"
|
||||
import "@/styles/index.scss"
|
||||
import "normalize.css"
|
||||
|
||||
const app = createApp(App)
|
||||
// 加载全局 svg
|
||||
loadSvg(app)
|
||||
// 自定义指令
|
||||
Object.keys(directives).forEach((key) => {
|
||||
app.directive(key, (directives as { [key: string]: Directive })[key])
|
||||
})
|
||||
|
||||
app.use(store).use(router).mount("#app")
|
@ -1,134 +0,0 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"
|
||||
const Layout = () => import("@/layout/index.vue")
|
||||
|
||||
/** 常驻路由 */
|
||||
export const constantRoutes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/redirect",
|
||||
component: Layout,
|
||||
meta: {
|
||||
hidden: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/redirect/:path(.*)",
|
||||
component: () => import("@/views/redirect/index.vue")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
redirect: "/dashboard",
|
||||
children: [
|
||||
{
|
||||
path: "dashboard",
|
||||
component: () => import("@/views/dashboard/index.vue"),
|
||||
name: "Dashboard",
|
||||
meta: {
|
||||
title: "首页",
|
||||
icon: "dashboard",
|
||||
affix: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 动态路由
|
||||
* 用来放置有权限(roles 属性)的路由
|
||||
* 必须带有 name 属性
|
||||
*/
|
||||
export const asyncRoutes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/permission",
|
||||
component: Layout,
|
||||
redirect: "/permission/page",
|
||||
name: "Permission",
|
||||
meta: {
|
||||
title: "权限管理",
|
||||
icon: "lock",
|
||||
roles: ["admin", "editor"], // 可以在根路由中设置角色
|
||||
alwaysShow: true // 将始终显示根菜单
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "page",
|
||||
component: () => import("@/views/permission/page.vue"),
|
||||
name: "PagePermission",
|
||||
meta: {
|
||||
title: "页面权限",
|
||||
roles: ["admin"] // 或者在子导航中设置角色
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "directive",
|
||||
component: () => import("@/views/permission/directive.vue"),
|
||||
name: "DirectivePermission",
|
||||
meta: {
|
||||
title: "指令权限" // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*", // 必须将 'ErrorPage' 路由放在最后, Must put the 'ErrorPage' route at the end
|
||||
component: Layout,
|
||||
redirect: "/404",
|
||||
name: "ErrorPage",
|
||||
meta: {
|
||||
title: "错误页面",
|
||||
icon: "404",
|
||||
hidden: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "401",
|
||||
component: () => import("@/views/error-page/401.vue"),
|
||||
name: "401",
|
||||
meta: {
|
||||
title: "401"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "404",
|
||||
component: () => import("@/views/error-page/404.vue"),
|
||||
name: "404",
|
||||
meta: {
|
||||
title: "404"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
/** 重置路由 */
|
||||
export function resetRouter() {
|
||||
// 注意:所有动态路由路由必须带有 name 属性,否则可能会不能完全重置干净
|
||||
try {
|
||||
router.getRoutes().forEach((route) => {
|
||||
const { name, meta } = route
|
||||
if (name && meta.roles?.length) {
|
||||
router.hasRoute(name) && router.removeRoute(name)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// 强制刷新浏览器,不过体验不是很好
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
export default router
|
@ -1,72 +0,0 @@
|
||||
import router from "@/router"
|
||||
import { RouteLocationNormalized } from "vue-router"
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { whiteList } from "@/config/white-list"
|
||||
import { getToken } from "@/utils/cookies"
|
||||
import asyncRouteSettings from "@/config/async-route"
|
||||
import NProgress from "nprogress"
|
||||
import "nprogress/nprogress.css"
|
||||
|
||||
const userStore = useUserStoreHook()
|
||||
const permissionStore = usePermissionStoreHook()
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
router.beforeEach(async (to: RouteLocationNormalized, _: RouteLocationNormalized, next: any) => {
|
||||
NProgress.start()
|
||||
// 判断该用户是否登录
|
||||
if (getToken()) {
|
||||
if (to.path === "/login") {
|
||||
// 如果登录,并准备进入 login 页面,则重定向到主页
|
||||
next({ path: "/" })
|
||||
NProgress.done()
|
||||
} else {
|
||||
// 检查用户是否已获得其权限角色
|
||||
if (userStore.roles.length === 0) {
|
||||
try {
|
||||
if (asyncRouteSettings.open) {
|
||||
// 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
|
||||
await userStore.getInfo()
|
||||
const roles = userStore.roles
|
||||
// 根据角色生成可访问的 routes(可访问路由 = 常驻路由 + 有访问权限的动态路由)
|
||||
permissionStore.setRoutes(roles)
|
||||
} else {
|
||||
// 没有开启动态路由功能,则启用默认角色
|
||||
userStore.setRoles(asyncRouteSettings.defaultRoles)
|
||||
permissionStore.setRoutes(asyncRouteSettings.defaultRoles)
|
||||
}
|
||||
// 将'有访问权限的动态路由' 添加到 router 中
|
||||
permissionStore.dynamicRoutes.forEach((route) => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
// 确保添加路由已完成
|
||||
// 设置 replace: true, 因此导航将不会留下历史记录
|
||||
next({ ...to, replace: true })
|
||||
} catch (err: any) {
|
||||
// 过程中发生任何错误,都直接重置 token,并重定向到登录页面
|
||||
userStore.resetToken()
|
||||
ElMessage.error(err.message || "路由守卫过程发生错误")
|
||||
next("/login")
|
||||
NProgress.done()
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有 token
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
// 如果在免登录的白名单中,则直接进入
|
||||
next()
|
||||
} else {
|
||||
// 其他没有访问权限的页面将被重定向到登录页面
|
||||
next("/login")
|
||||
NProgress.done()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
@ -1,5 +0,0 @@
|
||||
import { createPinia } from "pinia"
|
||||
|
||||
const store = createPinia()
|
||||
|
||||
export default store
|
@ -1,68 +0,0 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { getSidebarStatus, getActiveThemeName, setSidebarStatus, setActiveThemeName } from "@/utils/cookies"
|
||||
import themeList from "@/config/theme"
|
||||
|
||||
export enum DeviceType {
|
||||
Mobile,
|
||||
Desktop
|
||||
}
|
||||
|
||||
interface IAppState {
|
||||
device: DeviceType
|
||||
sidebar: {
|
||||
opened: boolean
|
||||
withoutAnimation: boolean
|
||||
}
|
||||
/** 主题列表 */
|
||||
themeList: { title: string; name: string }[]
|
||||
/** 正在应用的主题的名字 */
|
||||
activeThemeName: string
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: "app",
|
||||
state: (): IAppState => {
|
||||
return {
|
||||
device: DeviceType.Desktop,
|
||||
sidebar: {
|
||||
opened: getSidebarStatus() !== "closed",
|
||||
withoutAnimation: false
|
||||
},
|
||||
themeList: themeList,
|
||||
activeThemeName: getActiveThemeName() || "normal"
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
toggleSidebar(withoutAnimation: boolean) {
|
||||
this.sidebar.opened = !this.sidebar.opened
|
||||
this.sidebar.withoutAnimation = withoutAnimation
|
||||
if (this.sidebar.opened) {
|
||||
setSidebarStatus("opened")
|
||||
} else {
|
||||
setSidebarStatus("closed")
|
||||
}
|
||||
},
|
||||
closeSidebar(withoutAnimation: boolean) {
|
||||
this.sidebar.opened = false
|
||||
this.sidebar.withoutAnimation = withoutAnimation
|
||||
setSidebarStatus("closed")
|
||||
},
|
||||
toggleDevice(device: DeviceType) {
|
||||
this.device = device
|
||||
},
|
||||
setTheme(activeThemeName: string) {
|
||||
// 检查这个主题在主题列表里是否存在
|
||||
this.activeThemeName = this.themeList.find((theme) => theme.name === activeThemeName)
|
||||
? activeThemeName
|
||||
: this.themeList[0].name
|
||||
// 应用到 dom
|
||||
document.body.className = `theme-${this.activeThemeName}`
|
||||
// 持久化
|
||||
setActiveThemeName(this.activeThemeName)
|
||||
},
|
||||
initTheme() {
|
||||
// 初始化
|
||||
document.body.className = `theme-${this.activeThemeName}`
|
||||
}
|
||||
}
|
||||
})
|
@ -1,64 +0,0 @@
|
||||
import store from "@/store"
|
||||
import { defineStore } from "pinia"
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
import { constantRoutes, asyncRoutes } from "@/router"
|
||||
|
||||
interface IPermissionState {
|
||||
routes: RouteRecordRaw[]
|
||||
dynamicRoutes: RouteRecordRaw[]
|
||||
}
|
||||
|
||||
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||
if (route.meta && route.meta.roles) {
|
||||
return roles.some((role) => {
|
||||
if (route.meta?.roles !== undefined) {
|
||||
return route.meta.roles.includes(role)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||
const res: RouteRecordRaw[] = []
|
||||
routes.forEach((route) => {
|
||||
const r = { ...route }
|
||||
if (hasPermission(roles, r)) {
|
||||
if (r.children) {
|
||||
r.children = filterAsyncRoutes(r.children, roles)
|
||||
}
|
||||
res.push(r)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore({
|
||||
id: "permission",
|
||||
state: (): IPermissionState => {
|
||||
return {
|
||||
routes: [],
|
||||
dynamicRoutes: []
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setRoutes(roles: string[]) {
|
||||
let accessedRoutes
|
||||
if (roles.includes("admin")) {
|
||||
accessedRoutes = asyncRoutes
|
||||
} else {
|
||||
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
|
||||
}
|
||||
this.routes = constantRoutes.concat(accessedRoutes)
|
||||
this.dynamicRoutes = accessedRoutes
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 在 setup 外使用 */
|
||||
export function usePermissionStoreHook() {
|
||||
return usePermissionStore(store)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { defineStore } from "pinia"
|
||||
import layoutSettings from "@/config/layout"
|
||||
|
||||
interface ISettingsState {
|
||||
fixedHeader: boolean
|
||||
showSettings: boolean
|
||||
showTagsView: boolean
|
||||
showSidebarLogo: boolean
|
||||
showThemeSwitch: boolean
|
||||
showScreenfull: boolean
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore({
|
||||
id: "settings",
|
||||
state: (): ISettingsState => {
|
||||
return {
|
||||
fixedHeader: layoutSettings.fixedHeader,
|
||||
showSettings: layoutSettings.showSettings,
|
||||
showTagsView: layoutSettings.showTagsView,
|
||||
showSidebarLogo: layoutSettings.showSidebarLogo,
|
||||
showThemeSwitch: layoutSettings.showThemeSwitch,
|
||||
showScreenfull: layoutSettings.showScreenfull
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
changeSetting(payload: { key: string; value: any }) {
|
||||
const { key, value } = payload
|
||||
switch (key) {
|
||||
case "fixedHeader":
|
||||
this.fixedHeader = value
|
||||
break
|
||||
case "showSettings":
|
||||
this.showSettings = value
|
||||
break
|
||||
case "showSidebarLogo":
|
||||
this.showSidebarLogo = value
|
||||
break
|
||||
case "showTagsView":
|
||||
this.showTagsView = value
|
||||
break
|
||||
case "showThemeSwitch":
|
||||
this.showThemeSwitch = value
|
||||
break
|
||||
case "showScreenfull":
|
||||
this.showScreenfull = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,56 +0,0 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { _RouteLocationBase, RouteLocationNormalized } from "vue-router"
|
||||
|
||||
export interface ITagView extends Partial<RouteLocationNormalized> {
|
||||
title?: string
|
||||
to?: _RouteLocationBase
|
||||
}
|
||||
|
||||
interface ITagsViewState {
|
||||
visitedViews: ITagView[]
|
||||
}
|
||||
|
||||
export const useTagsViewStore = defineStore({
|
||||
id: "tags-view",
|
||||
state: (): ITagsViewState => {
|
||||
return {
|
||||
visitedViews: []
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addVisitedView(view: ITagView) {
|
||||
if (this.visitedViews.some((v) => v.path === view.path)) return
|
||||
this.visitedViews.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || "no-name"
|
||||
})
|
||||
)
|
||||
},
|
||||
delVisitedView(view: ITagView) {
|
||||
for (const [i, v] of this.visitedViews.entries()) {
|
||||
if (v.path === view.path) {
|
||||
this.visitedViews.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
delOthersVisitedViews(view: ITagView) {
|
||||
this.visitedViews = this.visitedViews.filter((v) => {
|
||||
return v.meta?.affix || v.path === view.path
|
||||
})
|
||||
},
|
||||
delAllVisitedViews() {
|
||||
// keep affix tags
|
||||
const affixTags = this.visitedViews.filter((tag) => tag.meta?.affix)
|
||||
this.visitedViews = affixTags
|
||||
},
|
||||
updateVisitedView(view: ITagView) {
|
||||
for (let v of this.visitedViews) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,89 +0,0 @@
|
||||
import store from "@/store"
|
||||
import { defineStore } from "pinia"
|
||||
import { usePermissionStore } from "./permission"
|
||||
import { getToken, removeToken, setToken } from "@/utils/cookies"
|
||||
import router, { resetRouter } from "@/router"
|
||||
import { accountLogin, userInfoRequest } from "@/api/login"
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
|
||||
interface IUserState {
|
||||
token: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
state: (): IUserState => {
|
||||
return {
|
||||
token: getToken() || "",
|
||||
roles: []
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/** 设置角色数组 */
|
||||
setRoles(roles: string[]) {
|
||||
this.roles = roles
|
||||
},
|
||||
/** 登录 */
|
||||
login(userInfo: { username: string; password: string }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
accountLogin({
|
||||
username: userInfo.username.trim(),
|
||||
password: userInfo.password
|
||||
})
|
||||
.then((res: any) => {
|
||||
setToken(res.data.accessToken)
|
||||
this.token = res.data.accessToken
|
||||
resolve(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 获取用户详情 */
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
userInfoRequest()
|
||||
.then((res: any) => {
|
||||
this.roles = res.data.user.roles
|
||||
resolve(res)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 切换角色 */
|
||||
async changeRoles(role: string) {
|
||||
const token = role + "-token"
|
||||
this.token = token
|
||||
setToken(token)
|
||||
await this.getInfo()
|
||||
const permissionStore = usePermissionStore()
|
||||
permissionStore.setRoutes(this.roles)
|
||||
resetRouter()
|
||||
permissionStore.dynamicRoutes.forEach((item: RouteRecordRaw) => {
|
||||
router.addRoute(item)
|
||||
})
|
||||
},
|
||||
/** 登出 */
|
||||
logout() {
|
||||
removeToken()
|
||||
this.token = ""
|
||||
this.roles = []
|
||||
resetRouter()
|
||||
},
|
||||
/** 重置 token */
|
||||
resetToken() {
|
||||
removeToken()
|
||||
this.token = ""
|
||||
this.roles = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 在 setup 外使用 */
|
||||
export function useUserStoreHook() {
|
||||
return useUserStore(store)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
@import "./mixins.scss"; // mixins
|
||||
@import "./transition.scss"; // transition
|
||||
@import "./theme/register.scss"; // 注册主题
|
||||
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #f0f2f5; // 全局背景色
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
@import "./setting.scss";
|
||||
@import "../theme.scss";
|
@ -1,14 +0,0 @@
|
||||
// 主题名称
|
||||
$theme-name: "dark";
|
||||
// 主题背景颜色
|
||||
$theme-bg-color: #151515;
|
||||
// active 状态下主题背景颜色
|
||||
$active-theme-bg-color: #409eff;
|
||||
// 默认文字颜色
|
||||
$font-color: #c0c4cc;
|
||||
// active 状态下文字颜色
|
||||
$active-font-color: #fff;
|
||||
// hover 状态下文字颜色
|
||||
$hover-color: #fff;
|
||||
// 边框颜色
|
||||
$border-color: #303133;
|
@ -1,2 +0,0 @@
|
||||
// 注册的主题
|
||||
@import "@/styles/theme/dark/index.scss";
|
@ -1,215 +0,0 @@
|
||||
.theme-#{$theme-name} {
|
||||
/** Layout */
|
||||
|
||||
.app-wrapper {
|
||||
background-color: $theme-bg-color;
|
||||
color: $font-color;
|
||||
|
||||
// 侧边栏
|
||||
.sidebar-container {
|
||||
.sidebar-logo-container {
|
||||
background-color: lighten($theme-bg-color, 2%) !important;
|
||||
}
|
||||
.el-menu {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
.el-menu-item {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
&.is-active,
|
||||
&:hover {
|
||||
background-color: lighten($theme-bg-color, 8%) !important;
|
||||
color: $active-font-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部导航栏
|
||||
.navbar {
|
||||
background-color: $theme-bg-color;
|
||||
.el-breadcrumb__inner {
|
||||
a {
|
||||
color: $font-color;
|
||||
&:hover {
|
||||
color: $hover-color;
|
||||
}
|
||||
}
|
||||
.no-redirect {
|
||||
color: $font-color;
|
||||
}
|
||||
}
|
||||
.right-menu {
|
||||
.el-icon {
|
||||
color: $font-color;
|
||||
}
|
||||
.el-avatar {
|
||||
background: lighten($theme-bg-color, 20%);
|
||||
.el-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tags-view
|
||||
.tags-view-container {
|
||||
background-color: $theme-bg-color !important;
|
||||
border-bottom: 1px solid lighten($theme-bg-color, 10%) !important;
|
||||
.tags-view-item {
|
||||
background-color: $theme-bg-color !important;
|
||||
color: $font-color !important;
|
||||
border: 1px solid $border-color !important;
|
||||
&.active {
|
||||
background-color: $active-theme-bg-color !important;
|
||||
color: $active-font-color !important;
|
||||
border-color: $border-color !important;
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
// 右键菜单
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
color: $font-color;
|
||||
li:hover {
|
||||
background-color: lighten($theme-bg-color, 16%);
|
||||
color: $active-font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧设置面板
|
||||
.handle-button {
|
||||
background-color: lighten($theme-bg-color, 20%) !important;
|
||||
}
|
||||
.el-drawer.rtl {
|
||||
background-color: $theme-bg-color;
|
||||
.drawer-title,
|
||||
.drawer-item {
|
||||
color: $font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** app-main 主要写 view 页面的黑暗样式 */
|
||||
|
||||
.app-main {
|
||||
// 指令权限页面 /permission/directive
|
||||
.permission-alert {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
}
|
||||
// 监控页面 /monitor
|
||||
.monitor {
|
||||
background-color: $theme-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
/** login 页面 */
|
||||
|
||||
.login-container {
|
||||
background-color: $theme-bg-color;
|
||||
color: $font-color;
|
||||
.login-card {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
}
|
||||
.el-icon {
|
||||
color: $font-color;
|
||||
}
|
||||
}
|
||||
|
||||
/** element-plus */
|
||||
|
||||
// 侧边栏的 item 的 popper
|
||||
.el-popper {
|
||||
border: none !important;
|
||||
.el-menu {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
.el-menu-item {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
&.is-active,
|
||||
&:hover {
|
||||
background-color: lighten($theme-bg-color, 8%) !important;
|
||||
color: $active-font-color !important;
|
||||
}
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
background-color: lighten($theme-bg-color, 4%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单
|
||||
.el-dropdown__popper .el-dropdown__list {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
.el-dropdown-menu {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
.el-dropdown-menu__item {
|
||||
color: $font-color;
|
||||
&.is-disabled {
|
||||
color: #606266;
|
||||
}
|
||||
&:not(.is-disabled):hover {
|
||||
background-color: lighten($theme-bg-color, 16%);
|
||||
color: $active-font-color;
|
||||
}
|
||||
}
|
||||
.el-dropdown-menu__item--divided:before {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-popper__arrow::before {
|
||||
// 下拉菜单顶部三角区域
|
||||
background-color: lighten($theme-bg-color, 8%) !important;
|
||||
border: lighten($theme-bg-color, 8%) !important;
|
||||
}
|
||||
|
||||
// 单选框按钮样式
|
||||
.el-radio-button__inner {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
color: $active-font-color;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
.el-radio-button:first-child .el-radio-button__inner {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
// el-tag
|
||||
.el-tag {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
border-color: $border-color;
|
||||
color: $active-font-color;
|
||||
&.el-tag--info {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
border-color: $border-color;
|
||||
color: $active-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
// tabs 标签页
|
||||
.el-tabs--border-card {
|
||||
background: lighten($theme-bg-color, 8%);
|
||||
border: 1px solid $border-color;
|
||||
.el-tabs__header {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
border-bottom: 1px solid $border-color;
|
||||
.el-tabs__item.is-active {
|
||||
background-color: lighten($theme-bg-color, 8%);
|
||||
border-right-color: $border-color;
|
||||
border-left-color: $border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片 card
|
||||
.el-card {
|
||||
background: lighten($theme-bg-color, 8%);
|
||||
border: 1px solid $border-color;
|
||||
color: $font-color;
|
||||
}
|
||||
|
||||
// 输入框 input
|
||||
.el-input__wrapper {
|
||||
background: lighten($theme-bg-color, 8%) !important;
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
// See https://vuejs.org/v2/guide/transitions.html for detail
|
||||
|
||||
// fade
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.28s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// fade-transform
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
// breadcrumb
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-move {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
/** cookies 封装 */
|
||||
|
||||
import Keys from "@/constant/key"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
export const getSidebarStatus = () => Cookies.get(Keys.sidebarStatus)
|
||||
export const setSidebarStatus = (sidebarStatus: string) => Cookies.set(Keys.sidebarStatus, sidebarStatus)
|
||||
|
||||
export const getToken = () => Cookies.get(Keys.token)
|
||||
export const setToken = (token: string) => Cookies.set(Keys.token, token)
|
||||
export const removeToken = () => Cookies.remove(Keys.token)
|
||||
|
||||
export const getActiveThemeName = () => Cookies.get(Keys.activeThemeName)
|
||||
export const setActiveThemeName = (themeName: string) => {
|
||||
Cookies.set(Keys.activeThemeName, themeName)
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import dayjs from "dayjs"
|
||||
|
||||
/** 格式化时间 */
|
||||
export const formatDateTime = (time: any) => {
|
||||
if (time == null || time === "") {
|
||||
return "N/A"
|
||||
}
|
||||
const date = new Date(time)
|
||||
return dayjs(date).format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
|
||||
/** 全局权限判断函数,和指令 v-permission 功能类似 */
|
||||
export const checkPermission = (value: string[]): boolean => {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const roles = useUserStoreHook().roles
|
||||
const permissionRoles = value
|
||||
return roles.some((role) => {
|
||||
return permissionRoles.includes(role)
|
||||
})
|
||||
} else {
|
||||
console.error("need roles! Like v-permission=\"['admin','editor']\"")
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from "axios"
|
||||
import { get } from "lodash-es"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { getToken } from "@/utils/cookies"
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
|
||||
/** 创建请求实例 */
|
||||
function createService() {
|
||||
// 创建一个 axios 实例
|
||||
const service = axios.create()
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config) => config,
|
||||
// 发送失败
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
// 响应拦截(可根据具体业务作出相应的调整)
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
// apiData 是 api 返回的数据
|
||||
const apiData = response.data as any
|
||||
// 这个 code 是和后端约定的业务 code
|
||||
const code = apiData.code
|
||||
// 如果没有 code, 代表这不是项目后端开发的 api
|
||||
if (code === undefined) {
|
||||
ElMessage.error("非本系统的接口")
|
||||
return Promise.reject(new Error("非本系统的接口"))
|
||||
} else {
|
||||
switch (code) {
|
||||
case 0:
|
||||
// code === 0 代表没有错误
|
||||
return apiData
|
||||
case 20000:
|
||||
// code === 20000 代表没有错误
|
||||
return apiData
|
||||
default:
|
||||
// 不是正确的 code
|
||||
ElMessage.error(apiData.msg || "Error")
|
||||
return Promise.reject(new Error("Error"))
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// status 是 HTTP 状态码
|
||||
const status = get(error, "response.status")
|
||||
switch (status) {
|
||||
case 400:
|
||||
error.message = "请求错误"
|
||||
break
|
||||
case 401:
|
||||
error.message = "未授权,请登录"
|
||||
break
|
||||
case 403:
|
||||
// token 过期时,直接退出登录并强制刷新页面(会重定向到登录页)
|
||||
useUserStoreHook().logout()
|
||||
location.reload()
|
||||
break
|
||||
case 404:
|
||||
error.message = "请求地址出错"
|
||||
break
|
||||
case 408:
|
||||
error.message = "请求超时"
|
||||
break
|
||||
case 500:
|
||||
error.message = "服务器内部错误"
|
||||
break
|
||||
case 501:
|
||||
error.message = "服务未实现"
|
||||
break
|
||||
case 502:
|
||||
error.message = "网关错误"
|
||||
break
|
||||
case 503:
|
||||
error.message = "服务不可用"
|
||||
break
|
||||
case 504:
|
||||
error.message = "网关超时"
|
||||
break
|
||||
case 505:
|
||||
error.message = "HTTP版本不受支持"
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
ElMessage.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return service
|
||||
}
|
||||
|
||||
/** 创建请求方法 */
|
||||
function createRequestFunction(service: AxiosInstance) {
|
||||
return function (config: AxiosRequestConfig) {
|
||||
const configDefault = {
|
||||
headers: {
|
||||
// 携带 token
|
||||
"X-Access-Token": getToken(),
|
||||
"Content-Type": get(config, "headers.Content-Type", "application/json")
|
||||
},
|
||||
timeout: 5000,
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
data: {}
|
||||
}
|
||||
return service(Object.assign(configDefault, config))
|
||||
}
|
||||
}
|
||||
|
||||
/** 用于网络请求的实例 */
|
||||
export const service = createService()
|
||||
/** 用于网络请求的方法 */
|
||||
export const request = createRequestFunction(service)
|
@ -1,14 +0,0 @@
|
||||
export const isExternal = (path: string) => /^(https?:|mailto:|tel:)/.test(path)
|
||||
|
||||
export const isArray = (arg: any) => {
|
||||
if (typeof Array.isArray === "undefined") {
|
||||
return Object.prototype.toString.call(arg) === "[object Array]"
|
||||
}
|
||||
return Array.isArray(arg)
|
||||
}
|
||||
|
||||
export const isValidURL = (url: string) => {
|
||||
const reg =
|
||||
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
|
||||
return reg.test(url)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">Admin 权限可见</div>
|
||||
</template>
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">Editor 权限可见</div>
|
||||
</template>
|
@ -1,20 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, ref } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import AdminDashboard from "./admin/index.vue"
|
||||
import EditorDashboard from "./editor/index.vue"
|
||||
|
||||
const currentRole = ref("admin")
|
||||
const roles = computed(() => {
|
||||
return useUserStore().roles
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
if (!roles.value.includes("admin")) {
|
||||
currentRole.value = "editor"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="currentRole === 'admin' ? AdminDashboard : EditorDashboard" />
|
||||
</template>
|
@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<p style="text-align: center; font-size: 140px; margin-bottom: 50px">401</p>
|
||||
<p style="text-align: center; font-size: 40px">你没有权限去该页面</p>
|
||||
</div>
|
||||
</template>
|
@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<p style="text-align: center; font-size: 140px; margin-bottom: 50px">404</p>
|
||||
<p style="text-align: center; font-size: 40px">未找到你想要的页面</p>
|
||||
</div>
|
||||
</template>
|
@ -1,180 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { User, Lock, Key } from "@element-plus/icons-vue"
|
||||
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
|
||||
|
||||
interface ILoginForm {
|
||||
/** admin 或 editor */
|
||||
username: string
|
||||
/** 密码 */
|
||||
password: string
|
||||
/** 验证码 */
|
||||
code: string
|
||||
/** 随机数 */
|
||||
codeToken: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const loginFormDom = ref<any>()
|
||||
const codeUrl = ref<string>("")
|
||||
const loginForm = reactive<ILoginForm>({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
code: "1234",
|
||||
codeToken: ""
|
||||
})
|
||||
const loginRules = reactive({
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ min: 6, max: 18, message: "长度在 6 到 18 个字符", trigger: "blur" }
|
||||
],
|
||||
code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
|
||||
})
|
||||
const handleLogin = () => {
|
||||
loginFormDom.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
useUserStore()
|
||||
.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = false
|
||||
router.push({ path: "/" }).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
createCode()
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
/** 创建验证码 */
|
||||
const createCode: () => void = () => {
|
||||
// 先清空验证码的输入
|
||||
loginForm.code = ""
|
||||
let codeToken = ""
|
||||
const codeTokenLength = 12
|
||||
// 随机数
|
||||
for (let i = 0; i < codeTokenLength; i++) {
|
||||
const index = Math.floor(Math.random() * 36)
|
||||
codeToken += index
|
||||
}
|
||||
loginForm.codeToken = codeToken
|
||||
// 实际开发中,可替换成自己的地址,这里只是提供一个参考
|
||||
codeUrl.value = `/api/v1/login/authcode?token=${codeToken}`
|
||||
}
|
||||
// 需要验证码的时候,需打开下方注释
|
||||
// createCode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<ThemeSwitch class="theme-switch" />
|
||||
<div class="login-card">
|
||||
<div class="title">
|
||||
<img src="@/assets/layout/logo-text-2.png" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-form ref="loginFormDom" :model="loginForm" :rules="loginRules" @keyup.enter="handleLogin">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
:prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
tabindex="2"
|
||||
:prefix-icon="Lock"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
placeholder="验证码"
|
||||
type="text"
|
||||
tabindex="3"
|
||||
:prefix-icon="Key"
|
||||
maxlength="4"
|
||||
size="large"
|
||||
/>
|
||||
<span class="show-code">
|
||||
<img :src="codeUrl" @click="createCode" />
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-button :loading="loading" type="primary" size="large" @click.prevent="handleLogin"> 登 录 </el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
.theme-switch {
|
||||
position: fixed;
|
||||
top: 5%;
|
||||
right: 5%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-card {
|
||||
width: 480px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 10px #dcdfe6;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 150px;
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 20px 50px 50px 50px;
|
||||
.show-code {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
img {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.el-button {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,28 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const emit = defineEmits(["change"])
|
||||
|
||||
const roles = computed(() => userStore.roles)
|
||||
const currentRole = ref(roles.value[0])
|
||||
watch(currentRole, async (value) => {
|
||||
await userStore.changeRoles(value)
|
||||
emit("change")
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom: 15px">你的权限:{{ roles }}</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<span>切换权限:</span>
|
||||
<el-radio-group v-model="currentRole">
|
||||
<el-radio-button label="editor" />
|
||||
<el-radio-button label="admin" />
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,85 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from "vue"
|
||||
import { checkPermission } from "@/utils/permission" // 权限判断函数
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
|
||||
const state = reactive({
|
||||
key: 1,
|
||||
handleRolesChange: () => {
|
||||
state.key++
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<SwitchRoles @change="state.handleRolesChange" />
|
||||
<div :key="state.key" style="margin-top: 30px">
|
||||
<div>
|
||||
<span v-permission="['admin']" class="permission-alert">
|
||||
只有
|
||||
<el-tag>admin</el-tag>可以看见这个
|
||||
</span>
|
||||
<el-tag v-permission="['admin']" class="permission-sourceCode" type="info" size="large">
|
||||
v-permission="['admin']"
|
||||
</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<span v-permission="['editor']" class="permission-alert">
|
||||
只有
|
||||
<el-tag>editor</el-tag>可以看见这个
|
||||
</span>
|
||||
<el-tag v-permission="['editor']" class="permission-sourceCode" type="info" size="large">
|
||||
v-permission="['editor']"
|
||||
</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<span v-permission="['admin', 'editor']" class="permission-alert">
|
||||
两者
|
||||
<el-tag>admin</el-tag>和 <el-tag>editor</el-tag>都可以看见这个
|
||||
</span>
|
||||
<el-tag v-permission="['admin', 'editor']" class="permission-sourceCode" type="info" size="large">
|
||||
v-permission="['admin', 'editor']"
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div :key="'checkPermission' + state.key" style="margin-top: 60px">
|
||||
<el-tag type="info" size="large">
|
||||
在某些情况下,不适合使用 v-permission。例如:element-plus 的 el-tab 或 el-table-column 以及其它动态渲染 dom
|
||||
的场景。你只能通过手动设置 v-if 来实现。
|
||||
</el-tag>
|
||||
<el-tabs type="border-card" style="width: 550px; margin-top: 60px">
|
||||
<el-tab-pane v-if="checkPermission(['admin'])" label="admin">
|
||||
admin 可以看见这个
|
||||
<el-tag class="permission-sourceCode" type="info"> v-if="checkPermission(['admin'])" </el-tag>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="checkPermission(['editor'])" label="editor">
|
||||
editor 可以看见这个
|
||||
<el-tag class="permission-sourceCode" type="info"> v-if="checkPermission(['editor'])" </el-tag>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="checkPermission(['admin', 'editor'])" label="admin 和 editor">
|
||||
两者 admin 和 editor 都可以看见这个
|
||||
<el-tag class="permission-sourceCode" type="info"> v-if="checkPermission(['admin', 'editor'])" </el-tag>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permission-alert {
|
||||
width: 320px;
|
||||
margin-top: 15px;
|
||||
background-color: #f0f9eb;
|
||||
color: #67c23a;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.permission-sourceCode {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
@ -1,19 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router"
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleRolesChange = () => {
|
||||
router.push({ path: "/401" }).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tag type="success" size="large" style="margin-bottom: 15px"> 当前页面只有 admin 权限可见 </el-tag>
|
||||
<SwitchRoles @change="handleRolesChange" />
|
||||
</div>
|
||||
</template>
|
@ -1,16 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
const { params, query } = useRoute()
|
||||
const { path } = params
|
||||
|
||||
useRouter()
|
||||
.replace({ path: "/" + path, query })
|
||||
.catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
@ -1,52 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
/** https://vitejs.cn/guide/features.html#typescript-compiler-options */
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
/** ts 严格模式 */
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
/** https://vitejs.cn/guide/features.html#typescript-compiler-options */
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node",
|
||||
"vite/client",
|
||||
/** element-plus 的 volar 插件支持 */
|
||||
"element-plus/global"
|
||||
],
|
||||
/** baseUrl 用来告诉编译器到哪里去查找模块,使用非相对模块时必须配置此项 */
|
||||
"baseUrl": ".",
|
||||
/** 非相对模块导入的路径映射配置,根据 baseUrl 配置进行路径计算 */
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"types/**/*.d.ts",
|
||||
"vite.config.ts"
|
||||
],
|
||||
/** 编译器默认排除的编译文件 */
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
6
types/auto-imports.d.ts
vendored
@ -1,6 +0,0 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
// We suggest you to commit this file into source control
|
||||
declare global {
|
||||
|
||||
}
|
||||
export {}
|
30
types/components.d.ts
vendored
@ -1,30 +0,0 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
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']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Screenfull: typeof import('./../src/components/Screenfull/index.vue')['default']
|
||||
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
|
||||
ThemeSwitch: typeof import('./../src/components/ThemeSwitch/index.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
6
types/env.d.ts
vendored
@ -1,6 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
/** 声明 vite 环境变量的类型(如果未声明则默认是 any) */
|
||||
declare interface ImportMetaEnv {
|
||||
readonly VITE_BASE_API: string
|
||||
}
|
9
types/shims-app.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
declare module "*.svg"
|
||||
declare module "*.png"
|
||||
declare module "*.jpg"
|
||||
declare module "*.jpeg"
|
||||
declare module "*.gif"
|
||||
declare module "*.bmp"
|
||||
declare module "*.tiff"
|
||||
declare module "*.yaml"
|
||||
declare module "*.json"
|
19
types/shims-vue.d.ts
vendored
@ -1,19 +0,0 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue"
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
export const gif: any
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module "*.scss" {
|
||||
const scss: Record<string, string>
|
||||
export default scss
|
||||
}
|
14
types/vue-proptery.d.ts
vendored
@ -1,14 +0,0 @@
|
||||
import { ElMessage } from "element-plus"
|
||||
|
||||
declare module "@vue/runtime-core" {
|
||||
interface ComponentCustomProperties {
|
||||
$message: ElMessage
|
||||
}
|
||||
}
|
||||
|
||||
declare module "vue-router" {
|
||||
interface RouteMeta {
|
||||
roles?: string[]
|
||||
activeMenu?: string
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
import { UserConfigExport } from "vite"
|
||||
import path, { resolve } from "path"
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import AutoImport from "unplugin-auto-import/vite"
|
||||
import Components from "unplugin-vue-components/vite"
|
||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
|
||||
import { createSvgIconsPlugin } from "vite-plugin-svg-icons"
|
||||
|
||||
/** 配置项文档:https://vitejs.dev/config */
|
||||
export default (): UserConfigExport => {
|
||||
return {
|
||||
/** build 打包时根据实际情况修改 base */
|
||||
base: "/",
|
||||
resolve: {
|
||||
alias: {
|
||||
/** @ 符号指向 src 目录 */
|
||||
"@": resolve(__dirname, "./src")
|
||||
}
|
||||
},
|
||||
server: {
|
||||
/** 是否开启 https */
|
||||
https: false,
|
||||
/** host 设置为 true 才可以使用 network 的形式,以 ip 访问项目 */
|
||||
host: true, // host: "0.0.0.0"
|
||||
/** 端口号 */
|
||||
port: 9999,
|
||||
/** 是否自动打开浏览器 */
|
||||
open: false,
|
||||
/** 跨域设置允许 */
|
||||
cors: true,
|
||||
/** 如果端口已占用,直接退出 */
|
||||
strictPort: true
|
||||
/** 接口代理 */
|
||||
// proxy: {
|
||||
// "/mock-api": {
|
||||
// target: "https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api",
|
||||
// ws: true,
|
||||
// /** 是否允许跨域 */
|
||||
// changeOrigin: true,
|
||||
// rewrite: (path) => path.replace("/mock-api", "")
|
||||
// }
|
||||
// }
|
||||
},
|
||||
build: {
|
||||
brotliSize: false,
|
||||
/** 消除打包大小超过 500kb 警告 */
|
||||
chunkSizeWarningLimit: 2000,
|
||||
/** vite 2.6.x 以上需要配置 minify: terser,terserOptions 才能生效 */
|
||||
minify: "terser",
|
||||
/** 在 build 代码时移除 console.log、debugger 和 注释 */
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: false,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ["console.log"]
|
||||
},
|
||||
output: {
|
||||
/** 删除注释 */
|
||||
comments: false
|
||||
}
|
||||
},
|
||||
assetsDir: "static/assets",
|
||||
/** 静态资源打包到 dist 下的不同目录 */
|
||||
rollupOptions: {
|
||||
output: {
|
||||
chunkFileNames: "static/js/[name]-[hash].js",
|
||||
entryFileNames: "static/js/[name]-[hash].js",
|
||||
assetFileNames: "static/[ext]/[name]-[hash].[ext]"
|
||||
}
|
||||
}
|
||||
},
|
||||
/** vite 插件 */
|
||||
plugins: [
|
||||
vue(),
|
||||
/** 自动按需导入 */
|
||||
AutoImport({
|
||||
dts: "./types/auto-imports.d.ts",
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
/** 自动按需导入 */
|
||||
Components({
|
||||
dts: "./types/components.d.ts",
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
/** svg */
|
||||
createSvgIconsPlugin({
|
||||
iconDirs: [path.resolve(process.cwd(), "src/icons/svg")],
|
||||
symbolId: "icon-[dir]-[name]"
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|