Template
1
0
mirror of https://github.com/un-pany/v3-admin-vite.git synced 2025-04-21 11:29:20 +08:00

mod: 清空该分支

This commit is contained in:
pany 2022-04-22 19:28:59 +08:00
parent f33e3ab16e
commit 96c9c7ad78
97 changed files with 0 additions and 7056 deletions

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1,7 +0,0 @@
# eslint 会忽略的文件
.DS_Store
node_modules
dist
dist-ssr
*.local

View File

@ -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
View File

@ -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

View File

@ -1,8 +0,0 @@
# prettier 会忽略的文件
.DS_Store
node_modules
dist
dist-ssr
*.local
*.d.ts

View File

@ -1,9 +0,0 @@
{
"recommendations": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"johnsoncodehk.vscode-typescript-vue-plugin",
"johnsoncodehk.volar"
]
}

21
LICENSE
View File

@ -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.

View File

@ -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
![v3-admin-vite.png](https://github.com/un-pany/v3-admin-vite/blob/main/src/assets/docs/qq.png)
## 📄 License
[MIT](https://github.com/un-pany/v3-admin-vite/blob/main/LICENSE)
Copyright (c) 2022 pany

View File

@ -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>

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -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>

View File

@ -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"
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -1,13 +0,0 @@
/** 注册的主题 */
const themeList = [
{
title: "默认",
name: "normal"
},
{
title: "黑暗",
name: "dark"
}
]
export default themeList

View File

@ -1,4 +0,0 @@
/** 免登录白名单 */
const whiteList = ["/login"]
export { whiteList }

View File

@ -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

View File

@ -1 +0,0 @@
export * from "./permission"

View File

@ -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']\"")
}
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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
}
}

View File

@ -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")

View File

@ -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

View File

@ -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()
})

View File

@ -1,5 +0,0 @@
import { createPinia } from "pinia"
const store = createPinia()
export default store

View File

@ -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}`
}
}
})

View File

@ -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)
}

View File

@ -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
}
}
}
})

View File

@ -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
}
}
}
}
})

View File

@ -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)
}

View File

@ -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;
}

View File

@ -1,7 +0,0 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}

View File

@ -1,2 +0,0 @@
@import "./setting.scss";
@import "../theme.scss";

View File

@ -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;

View File

@ -1,2 +0,0 @@
// 注册的主题
@import "@/styles/theme/dark/index.scss";

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -1,3 +0,0 @@
<template>
<div class="app-container">Admin 权限可见</div>
</template>

View File

@ -1,3 +0,0 @@
<template>
<div class="app-container">Editor 权限可见</div>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
]
}

View File

@ -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
View File

@ -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
View File

@ -1,6 +0,0 @@
/// <reference types="vite/client" />
/** 声明 vite 环境变量的类型(如果未声明则默认是 any */
declare interface ImportMetaEnv {
readonly VITE_BASE_API: string
}

View File

@ -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
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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: terserterserOptions 才能生效 */
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]"
})
]
}
}