This commit is contained in:
2023-08-09 22:15:16 +08:00
commit addd2ab9d2
36 changed files with 13754 additions and 0 deletions

33
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,33 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
"plugin:vue/vue3-essential",
"standard-with-typescript",
"plugin:prettier/recommended"
],
overrides: [],
parserOptions: {
project: "./tsconfig*.json",
ecmaVersion: "latest",
sourceType: "module"
},
plugins: [
"vue",
"prettier"
],
rules: {
"no-cond-assign": "error",
"eqeqeq": "error",
"indent": ["error", 4, {"SwitchCase": 1}],
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
]
}
};

55
.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
### Auto Imports ###
src/auto-imports.d.ts
src/components.d.ts

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 4,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Framework</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

12210
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "pinnacle-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev-host": "vite --host 0.0.0.0",
"build-only": "vite build",
"build": "run-p type-check build-only",
"preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.4.0",
"element-plus": "^2.3.4",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"pinia": "^2.0.36",
"vite-plugin-inspect": "^0.7.24",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.10",
"@types/jsdom": "^21.1.0",
"@types/node": "^18.14.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@vitejs/plugin-vue": "^4.2.1",
"@vue/test-utils": "^2.3.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.11.0",
"jsdom": "^21.1.0",
"mockjs": "^1.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.8",
"stylelint-config-prettier": "^9.0.5",
"typescript": "^5.0.4",
"unplugin-auto-import": "^0.15.3",
"unplugin-element-plus": "^0.7.1",
"unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.24.1",
"vite": "^4.3.9",
"vitest": "^0.30.1",
"vue-tsc": "^1.6.1"
}
}

4
public/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="200" height="200" fill="#409eff">
<path d="M810.554166 57.547768H213.848945c-86.156711 0-156.281057 70.126346-156.281057 156.280058v596.704221c0 86.208692 70.123347 156.279058 156.281057 156.279058h596.704221c86.207693 0 156.277059-70.070366 156.277059-156.279058V213.827826c0.001-86.152712-70.068367-156.280058-156.276059-156.280058z m71.037026 752.984279a71.061018 71.061018 0 0 1-71.034027 71.036027H213.848945a71.097005 71.097005 0 0 1-71.037026-71.036027v-56.26122L289.772254 567.359538a70.753126 70.753126 0 0 1 54.3289-27.106471c19.263228 0.2839 41.485415 8.524003 55.406521 24.66333l17.676785 20.513788a28.734898 28.734898 0 0 0 2.099262 2.499121l117.408724 136.73393a42.63901 42.63901 0 0 0 64.727244-55.522481l-95.696357-111.440821c52.28162-66.776524 112.577422-144.685134 125.248967-161.05238a70.752126 70.752126 0 0 1 54.210942-26.88055c22.901949 0.68176 41.430435 8.637963 55.29656 24.663329l137.18577 159.74384c1.30854 1.420501 2.554102 2.784021 3.920622 4.034582z m0-382.630481l-76.434128-88.993713a155.645281 155.645281 0 0 0-118.431364-54.380882c-1.140599 0-2.163239 0-3.240861 0.05698-46.768558 0.90968-90.300254 22.447108-116.045203 55.464501-1.249561 1.30754-4.598383 5.343122-5.682002 6.763622-10.396345 13.468265-64.043485 82.742911-113.714023 146.277574-29.09877-25.232129-65.976805-40.405795-105.817798-38.018634a155.684267 155.684267 0 0 0-119.450006 59.557062l-79.959889 101.664259v-402.458511a71.134992 71.134992 0 0 1 71.038026-71.037026h596.704221a71.099004 71.099004 0 0 1 71.033027 71.031028z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

19
src/App.vue Normal file
View File

@@ -0,0 +1,19 @@
<template>
<el-config-provider :locale="zhCn()">
<router-view></router-view>
</el-config-provider>
</template>
<script lang="ts">
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
export default {
methods: {
zhCn() {
return zhCn
}
}
}
</script>
<style scoped></style>

63
src/assets/css/base.css Normal file
View File

@@ -0,0 +1,63 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
em,
i {
font-style: normal
}
li {
list-style: none
}
img {
border: 0;
vertical-align: middle
}
button {
cursor: pointer
}
a {
color: #666;
text-decoration: none
}
button,
input {
font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
border: 0;
outline: none;
}
body {
-webkit-font-smoothing: antialiased;
background-color: #fff;
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
color: #666
}
.hide,
.none {
display: none
}
.clearfix:after {
visibility: hidden;
clear: both;
display: block;
content: ".";
height: 0
}
.clearfix {
*zoom: 1
}

125
src/assets/css/common.css Normal file
View File

@@ -0,0 +1,125 @@
:root {
--main-color: #409eff;
--background-color: #F5F5F5;
--font-main-color: #4D4D4D;
--font-secondary-color: #9E9E9E;
}
.body {
color: var(--font-main-color);
user-select: none;
min-width: 1100px;
min-height: 400px;
}
.fill {
height: 100%;
width: 100%;
}
.fill-with {
width: 100%;
}
.fill-height {
height: 100%;
}
.background-white {
background-color: white;
}
.center-box {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-center-box {
display: flex;
align-items: center;
}
.horizontal-center-box {
display: flex;
justify-content: center;
}
.icon-size-xs {
width: 16px;
height: 16px;
}
.icon-size-xs > use {
width: 16px;
height: 16px;
}
.icon-size-sm {
width: 20px;
height: 20px;
}
.icon-size-sm > use {
width: 20px;
height: 20px;
}
.icon-size-md {
width: 24px;
height: 24px;
}
.icon-size-md > use {
width: 24px;
height: 24px;
}
.icon-size-lg {
width: 32px;
height: 32px;
}
.icon-size-lg > use {
width: 32px;
height: 32px;
}
.icon-size-xl {
width: 64px;
height: 64px;
}
.icon-size-xl > use {
width: 64px;
height: 64px;
}
.icon-size-menu {
width: 23px;
height: 23px;
}
.icon-size-menu > use {
width: 23px;
height: 23px;
}
.el-message-box__btns button:first-child {
margin-left: 10px;
order: 1;
}
.el-popconfirm__action button:first-child {
margin-left: 12px;
float: right;
}
.el-popconfirm__action button:last-child {
margin-left: 0;
}
.el-table__cell .el-tag {
margin-left: 5px;
margin-top: 2px;
}

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

1
src/assets/svg/home.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough"><path d="M11.2633,0.229798C11.6966,-0.0765992,12.3034,-0.0765992,12.7367,0.229798C12.7367,0.229798,23.5367,7.86616,23.5367,7.86616C23.829,8.07284,24,8.39063,24,8.72727C24,8.72727,24,20.7273,24,20.7273C24,21.5953,23.6207,22.4277,22.9456,23.0414C22.2704,23.6552,21.3548,24,20.4,24C20.4,24,3.6,24,3.6,24C2.64522,24,1.72955,23.6552,1.05442,23.0414C0.379284,22.4277,0,21.5953,0,20.7273C0,20.7273,0,8.72727,0,8.72727C0,8.39063,0.170968,8.07284,0.463271,7.86616C0.463271,7.86616,11.2633,0.229798,11.2633,0.229798C11.2633,0.229798,11.2633,0.229798,11.2633,0.229798ZM9.6,21.8182C9.6,21.8182,14.4,21.8182,14.4,21.8182C14.4,21.8182,14.4,13.0909,14.4,13.0909C14.4,13.0909,9.6,13.0909,9.6,13.0909C9.6,13.0909,9.6,21.8182,9.6,21.8182C9.6,21.8182,9.6,21.8182,9.6,21.8182ZM16.8,21.8182C16.8,21.8182,16.8,12,16.8,12C16.8,11.3975,16.2628,10.9091,15.6,10.9091C15.6,10.9091,8.4,10.9091,8.4,10.9091C7.73726,10.9091,7.2,11.3975,7.2,12C7.2,12,7.2,21.8182,7.2,21.8182C7.2,21.8182,3.6,21.8182,3.6,21.8182C3.28174,21.8182,2.97652,21.7032,2.75147,21.4987C2.52643,21.2941,2.4,21.0166,2.4,20.7273C2.4,20.7273,2.4,9.26081,2.4,9.26081C2.4,9.26081,12,2.47294,12,2.47294C12,2.47294,21.6,9.26081,21.6,9.26081C21.6,9.26081,21.6,20.7273,21.6,20.7273C21.6,21.0166,21.4735,21.2941,21.2485,21.4987C21.0235,21.7032,20.7182,21.8182,20.4,21.8182C20.4,21.8182,16.8,21.8182,16.8,21.8182C16.8,21.8182,16.8,21.8182,16.8,21.8182Z" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

4
src/assets/svg/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="200" height="200">
<path d="M810.554166 57.547768H213.848945c-86.156711 0-156.281057 70.126346-156.281057 156.280058v596.704221c0 86.208692 70.123347 156.279058 156.281057 156.279058h596.704221c86.207693 0 156.277059-70.070366 156.277059-156.279058V213.827826c0.001-86.152712-70.068367-156.280058-156.276059-156.280058z m71.037026 752.984279a71.061018 71.061018 0 0 1-71.034027 71.036027H213.848945a71.097005 71.097005 0 0 1-71.037026-71.036027v-56.26122L289.772254 567.359538a70.753126 70.753126 0 0 1 54.3289-27.106471c19.263228 0.2839 41.485415 8.524003 55.406521 24.66333l17.676785 20.513788a28.734898 28.734898 0 0 0 2.099262 2.499121l117.408724 136.73393a42.63901 42.63901 0 0 0 64.727244-55.522481l-95.696357-111.440821c52.28162-66.776524 112.577422-144.685134 125.248967-161.05238a70.752126 70.752126 0 0 1 54.210942-26.88055c22.901949 0.68176 41.430435 8.637963 55.29656 24.663329l137.18577 159.74384c1.30854 1.420501 2.554102 2.784021 3.920622 4.034582z m0-382.630481l-76.434128-88.993713a155.645281 155.645281 0 0 0-118.431364-54.380882c-1.140599 0-2.163239 0-3.240861 0.05698-46.768558 0.90968-90.300254 22.447108-116.045203 55.464501-1.249561 1.30754-4.598383 5.343122-5.682002 6.763622-10.396345 13.468265-64.043485 82.742911-113.714023 146.277574-29.09877-25.232129-65.976805-40.405795-105.817798-38.018634a155.684267 155.684267 0 0 0-119.450006 59.557062l-79.959889 101.664259v-402.458511a71.134992 71.134992 0 0 1 71.038026-71.037026h596.704221a71.099004 71.099004 0 0 1 71.033027 71.031028z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="27.428573608398438" height="32.000003814697266"
viewBox="0 0 27.428573608398438 32.000003814697266"><g><path d="M22.8571,10.6667L25.9048,10.6667C26.7463,10.6667,27.4286,11.3489,27.4286,12.1905L27.4286,30.4762C27.4286,31.3178,26.7463,32,25.9048,32L1.52381,32C0.682233,32,0,31.3178,0,30.4762L0,12.1905C0,11.3489,0.682233,10.6667,1.52381,10.6667L4.57143,10.6667L4.57143,9.14286C4.57143,4.0934,8.66483,0,13.7143,0C18.7637,0,22.8571,4.0934,22.8571,9.14286L22.8571,10.6667ZM19.8095,10.6667L19.8095,9.14286C19.8095,5.77655,17.0806,3.04762,13.7143,3.04762C10.348,3.04762,7.61905,5.77655,7.61905,9.14286L7.61905,10.6667L19.8095,10.6667ZM12.1905,19.8095L12.1905,22.8571L15.2381,22.8571L15.2381,19.8095L12.1905,19.8095ZM6.09524,19.8095L6.09524,22.8571L9.14286,22.8571L9.14286,19.8095L6.09524,19.8095ZM18.2857,19.8095L18.2857,22.8571L21.3333,22.8571L21.3333,19.8095L18.2857,19.8095Z" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 966 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="200" height="200">
<path d="M782.954667 240.512C713.365333 170.901333 617.834667 127.701333 511.765333 127.701333 299.605333 127.701333 128.234667 299.541333 128.234667 511.701333 128.234667 723.861333 299.605333 895.701333 511.765333 895.701333 690.794667 895.701333 840.085333 773.312 882.794667 607.701333L782.954667 607.701333C743.594667 719.552 637.034667 799.701333 511.765333 799.701333 352.874667 799.701333 223.765333 670.592 223.765333 511.701333 223.765333 352.832 352.874667 223.701333 511.765333 223.701333 591.445333 223.701333 662.485333 256.832 714.325333 309.141333L559.765333 463.701333 895.765333 463.701333 895.765333 127.701333 782.954667 240.512Z"/>
</svg>

After

Width:  |  Height:  |  Size: 785 B

View File

@@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="200" height="200">
<path d="M920.419872 418.785859l-76.229871 0c-7.654882-26.792088-18.339822-52.308363-31.735867-76.070394l54.859991-54.859991c24.718891-24.718891 24.718891-64.58807 0-89.306962l-44.653481-44.653481c-24.718891-24.718891-64.58807-24.718891-89.306962 0l-55.816851 55.816851c-22.32674-12.279707-46.248248-21.84831-71.286093-28.865286L606.25074 104.616726c0-34.765924-28.067902-62.833827-62.833827-62.833827l-62.833827 0c-34.765924 0-62.833827 28.067902-62.833827 62.833827l0 76.070394c-25.835228 7.335929-50.554119 17.542439-73.678243 30.300576l-57.411618-57.411618c-24.718891-24.718891-64.58807-24.718891-89.306962 0l-44.653481 44.653481c-24.718891 24.718891-24.718891 64.58807 0 89.306962l57.411618 57.411618c-12.917614 23.124124-22.964647 47.683538-30.300576 73.678243L103.580128 418.626382c-34.765924 0-62.833827 28.067902-62.833827 62.833827l0 62.833827c0 34.765924 28.067902 62.833827 62.833827 62.833827l76.229871 0c7.016976 24.878368 16.585579 48.959352 28.865286 71.286093l-55.816851 55.816851c-24.718891 24.718891-24.718891 64.58807 0 89.306962l44.653481 44.653481c24.718891 24.718891 64.58807 24.718891 89.306962 0l54.859991-54.859991c23.762031 13.555521 49.278306 24.240461 76.070394 31.735867l0 76.229871c0 34.765924 28.067902 62.833827 62.833827 62.833827l62.833827 0c34.765924 0 62.833827-28.067902 62.833827-62.833827L606.25074 845.2266c25.835228-7.335929 50.554119-17.542439 73.678243-30.300576l53.4247 53.4247c24.718891 24.718891 64.58807 24.718891 89.306962 0l44.653481-44.653481c24.718891-24.718891 24.718891-64.58807 0-89.306962L813.889425 680.965582c12.917614-23.124124 22.964647-47.683538 30.300576-73.678243l76.229871 0c34.765924 0 62.833827-28.227379 62.833827-62.833827L983.253699 481.619685C983.253699 447.013238 955.02632 418.785859 920.419872 418.785859M512 701.538078c-104.138296 0-188.50148-84.363183-188.50148-188.50148 0-104.138296 84.363183-188.50148 188.50148-188.50148 104.138296 0 188.50148 84.363183 188.50148 188.50148C700.50148 617.174895 616.138296 701.538078 512 701.538078M512 418.785859c-51.98941 0-94.25074 42.26133-94.25074 94.25074 0 51.98941 42.26133 94.25074 94.25074 94.25074 51.98941 0 94.25074-42.26133 94.25074-94.25074C606.25074 461.047189 563.98941 418.785859 512 418.785859"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

2
src/assets/svg/user.svg Normal file
View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="30.231319427490234" height="32"
viewBox="0 0 30.231319427490234 32"><g><path d="M29.0453,26.1146C28.2855,24.3136,27.192,22.6975,25.8051,21.3106C24.4182,19.9236,22.8021,18.8342,21.0011,18.0704C20.985,18.0623,20.9689,18.0583,20.9528,18.0503C23.4574,16.2412,25.0855,13.2945,25.0855,9.96985C25.0855,4.46231,20.6232,0,15.1157,0C9.60812,0,5.14581,4.46231,5.14581,9.96985C5.14581,13.2945,6.77395,16.2412,9.27847,18.0543C9.26239,18.0623,9.24631,18.0663,9.23023,18.0744C7.42923,18.8342,5.81315,19.9236,4.42621,21.3146C3.03928,22.7015,1.94983,24.3176,1.18601,26.1186C0.438272,27.8794,0.0402826,29.7487,0.0000815847,31.6704C-0.00393876,31.8513,0.140785,32,0.32169,32L2.73375,32C2.91063,32,3.05134,31.8593,3.05536,31.6864C3.13576,28.5829,4.38199,25.6764,6.58501,23.4734C8.8644,21.194,11.8915,19.9397,15.1157,19.9397C18.3398,19.9397,21.3669,21.194,23.6463,23.4734C25.8493,25.6764,27.0956,28.5829,27.176,31.6864C27.18,31.8633,27.3207,32,27.4976,32L29.9096,32C30.0905,32,30.2353,31.8513,30.2312,31.6704C30.191,29.7487,29.793,27.8794,29.0453,26.1146ZM15.1157,16.8844C13.2704,16.8844,11.5338,16.1648,10.2272,14.8583C8.92068,13.5518,8.20109,11.8151,8.20109,9.96985C8.20109,8.12462,8.92068,6.38794,10.2272,5.08141C11.5338,3.77487,13.2704,3.05528,15.1157,3.05528C16.9609,3.05528,18.6976,3.77487,20.0041,5.08141C21.3106,6.38794,22.0302,8.12462,22.0302,9.96985C22.0302,11.8151,21.3106,13.5518,20.0041,14.8583C18.6976,16.1648,16.9609,16.8844,15.1157,16.8844Z" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank">create-vue</a>, the
official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,83 @@
const PRODUCTION_NAME = 'Framework'
const TOKEN_NAME = 'JWT_TOKEN'
const COLOR_PRODUCTION = '#409eff'
const COLOR_BACKGROUND = '#F5F5F5'
const COLOR_FONT_MAIN = '#4D4D4D'
const COLOR_FONT_SECONDARY = '#9E9E9E'
const SIZE_ICON_XS = '16px'
const SIZE_ICON_SM = '20px'
const SIZE_ICON_MD = '24px'
const SIZE_ICON_LG = '32px'
const SIZE_ICON_XL = '64px'
// Response Code
const SYSTEM_OK = 20000
const LOGIN_SUCCESS = 20010
const LOGIN_USERNAME_PASSWORD_ERROR = 20011
const OLD_PASSWORD_NOT_MATCH = 20012
const LOGOUT_SUCCESS = 20015
const LOGOUT_FAILED = 20016
const TOKEN_IS_ILLEGAL = 20017
const TOKEN_HAS_EXPIRED = 20018
const TOKEN_RENEW_SUCCESS = 20019
const DATABASE_SELECT_OK = 20021
const DATABASE_SAVE_OK = 20022
const DATABASE_UPDATE_OK = 20023
const DATABASE_DELETE_OK = 20024
const DATABASE_SELECT_ERROR = 20031
const DATABASE_SAVE_ERROR = 20032
const DATABASE_UPDATE_ERROR = 20033
const DATABASE_DELETE_ERROR = 20034
const DATABASE_TIMEOUT_ERROR = 20035
const DATABASE_CONNECT_ERROR = 20036
const DATABASE_DATA_TO_LONG = 20037
const DATABASE_DATA_VALIDATION_FAILED = 20038
const DATABASE_EXECUTE_ERROR = 20039
const UNAUTHORIZED = 30010
const ACCESS_DENIED = 30030
const USER_DISABLE = 30031
const SYSTEM_ERROR = 50001
const SYSTEM_TIMEOUT = 50002
export {
PRODUCTION_NAME,
TOKEN_NAME,
COLOR_PRODUCTION,
COLOR_BACKGROUND,
COLOR_FONT_MAIN,
COLOR_FONT_SECONDARY,
SIZE_ICON_XS,
SIZE_ICON_SM,
SIZE_ICON_MD,
SIZE_ICON_LG,
SIZE_ICON_XL,
SYSTEM_OK,
LOGIN_SUCCESS,
LOGIN_USERNAME_PASSWORD_ERROR,
OLD_PASSWORD_NOT_MATCH,
LOGOUT_SUCCESS,
LOGOUT_FAILED,
TOKEN_IS_ILLEGAL,
TOKEN_HAS_EXPIRED,
TOKEN_RENEW_SUCCESS,
DATABASE_SELECT_OK,
DATABASE_SAVE_OK,
DATABASE_UPDATE_OK,
DATABASE_DELETE_OK,
DATABASE_SELECT_ERROR,
DATABASE_SAVE_ERROR,
DATABASE_UPDATE_ERROR,
DATABASE_DELETE_ERROR,
DATABASE_TIMEOUT_ERROR,
DATABASE_CONNECT_ERROR,
DATABASE_DATA_TO_LONG,
DATABASE_DATA_VALIDATION_FAILED,
DATABASE_EXECUTE_ERROR,
UNAUTHORIZED,
ACCESS_DENIED,
USER_DISABLE,
SYSTEM_ERROR,
SYSTEM_TIMEOUT
}

15
src/main.ts Normal file
View File

@@ -0,0 +1,15 @@
import { createApp } from 'vue'
import App from '@/App.vue'
import router from '@/router'
import '@/assets/css/base.css'
import '@/assets/css/common.css'
import 'element-plus/theme-chalk/el-message.css'
import 'element-plus/theme-chalk/el-message-box.css'
import { createPinia } from 'pinia'
const app = createApp(App)
const pinia = createPinia()
app.use(router).use(pinia).mount('#app')

5
src/pages/Home.vue Normal file
View File

@@ -0,0 +1,5 @@
<script></script>
<template></template>
<style scoped></style>

11
src/pages/Login.vue Normal file
View File

@@ -0,0 +1,11 @@
<template></template>
<script lang="ts">
export default {
name: 'LoginPage',
data() {},
methods: {}
}
</script>
<style scoped></style>

370
src/pages/Main.vue Normal file
View File

@@ -0,0 +1,370 @@
<template>
<el-backtop target=".main-box" :right="80" :bottom="80" />
<el-scrollbar style="height: 100vh; width: 100vw">
<div class="body">
<div class="background">
<el-container class="fill">
<el-aside width="collapse" class="background-white aside">
<el-scrollbar>
<el-menu
:collapse="isCollapsed"
:unique-opened="true"
:default-active="
this.$route.path.indexOf(
'/',
this.$route.path.indexOf('/') + 1
) !== -1
? this.$route.path.indexOf(
'/',
this.$route.path.indexOf(
'/',
this.$route.path.indexOf('/') + 1
) + 1
) !== -1
? this.$route.path.substring(
0,
this.$route.path.indexOf(
'/',
this.$route.path.indexOf(
'/',
this.$route.path.indexOf('/') + 1
) + 1
)
)
: this.$route.path
: this.$route.path
"
:router="true"
class="menu"
:text-color="COLOR_FONT_MAIN()"
:active-text-color="COLOR_PRODUCTION()"
>
<el-menu-item
@mousedown.left="changeCollapsed"
:disabled="true"
style="
cursor: pointer;
opacity: 1;
border-bottom: 1px #ddd solid;
"
>
<el-icon :size="SIZE_ICON_LG()">
<icon-framework-logo :color="COLOR_PRODUCTION()" />
</el-icon>
<template #title>
<span class="menu-production-name">
{{ PRODUCTION_NAME() }}
</span>
</template>
</el-menu-item>
<template v-for="(route, index) in routes">
<el-menu-item
v-if="!route.children"
:key="index"
:index="route.path ?? ''"
>
<el-icon>
<component :is="route.meta.icon" />
</el-icon>
<template #title>{{ route.meta.title }}</template>
</el-menu-item>
<el-sub-menu
v-if="route.children"
:key="index"
:index="route.path ?? ''"
>
<template #title>
<el-icon>
<component :is="route.meta.icon" />
</el-icon>
<span>{{ route.meta.title }}</span>
</template>
<el-menu-item
v-for="(sub, index) in route.children"
:key="index"
:index="
sub.path
? route.path
? route.path + '/' + sub.path
: ''
: ''
"
>
<el-icon>
<component :is="sub.meta.icon" />
</el-icon>
<template #title>{{ sub.meta.title }}</template>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-scrollbar>
</el-aside>
<el-container>
<el-header height="56px" class="background-white main-header">
<template v-if="isLogin">
<el-divider direction="vertical" />
<el-popover
transition="el-zoom-in-top"
popper-style="box-shadow: rgb(14 18 22 / 20%) 0px 10px 38px -10px, rgb(14 18 22 / 20%) 0px 10px 20px -15px;"
>
<template #reference>
<div style="display: flex">
<div class="user-head">
<el-avatar>
<el-icon
:size="SIZE_ICON_SM()"
:color="COLOR_FONT_MAIN()"
>
<icon-framework-user />
</el-icon>
</el-avatar>
</div>
<div class="user-info">
<div class="user-name">
<span>{{ username }}</span>
</div>
<div class="user-desc">
<span>用户介绍</span>
</div>
</div>
</div>
</template>
<template #default>
<div
style="display: flex; gap: 10px; flex-direction: column"
>
<div>
<el-button
@click="handleProfile"
style="width: 100%"
>个人档案</el-button
>
</div>
<div>
<el-button @click="handleLogout" style="width: 100%"
>退出</el-button
>
</div>
</div>
</template>
</el-popover>
</template>
<el-button type="primary" link v-else>登录</el-button>
</el-header>
<ElScrollbar v-if="$route.meta.requiresScrollbar">
<ElMain
class="main-box"
:class="{ noPadding: !$route.meta.requiresPadding }"
>
<ElBacktop :right="100" :bottom="100" />
<RouterView></RouterView>
</ElMain>
</ElScrollbar>
<ElMain
v-else
class="main-box"
:class="{ noPadding: !$route.meta.requiresPadding }"
>
<ElBacktop :right="100" :bottom="100" />
<RouterView></RouterView>
</ElMain>
</el-container>
</el-container>
</div>
</div>
</el-scrollbar>
</template>
<script lang="ts">
import {
COLOR_FONT_MAIN,
COLOR_PRODUCTION,
PRODUCTION_NAME,
SIZE_ICON_LG,
SIZE_ICON_SM
} from '@/constants/Common.constants.js'
import _ from 'lodash'
import { getLoginStatus, getUser, getUsername, logout } from '@/utils/auth'
import { ElMessage } from 'element-plus'
import { getLocalStorage, setLocalStorage } from '@/utils/common'
export default {
name: 'MainFrame',
data() {
return {
isLogin: false,
routes: [],
isCollapsed: getLocalStorage('menuCollapsed') === 'true',
username: ''
}
},
methods: {
SIZE_ICON_LG() {
return SIZE_ICON_LG
},
PRODUCTION_NAME() {
return PRODUCTION_NAME
},
SIZE_ICON_SM() {
return SIZE_ICON_SM
},
COLOR_PRODUCTION() {
return COLOR_PRODUCTION
},
COLOR_FONT_MAIN() {
return COLOR_FONT_MAIN
},
handleLogout() {
logout()
ElMessage.success({
dangerouslyUseHTMLString: true,
message: '<strong>退出登录</strong>'
})
setTimeout(() => {
// this.$router.push({ name: 'Login' })
location.reload()
}, 1500)
},
changeCollapsed() {
this.isCollapsed = !this.isCollapsed
setLocalStorage('menuCollapsed', this.isCollapsed.toString())
},
handleProfile() {
this.$router.push('/profile')
}
},
async mounted() {
let user
if (getLoginStatus()) {
this.isLogin = true
this.username = await getUsername()
user = await getUser()
}
const allRoutes = _.cloneDeep(
_.filter(_.get(this.$router, 'options.routes[0].children'), 'meta.requiresMenu')
)
const menus = user?.menus
this.routes = allRoutes.filter((level1) => {
if (level1.meta.requiresMenuAuth) {
for (const menu of menus) {
if (_.startsWith(menu.url, level1.path)) {
let hasChildren = false
if (level1.children === undefined) {
return true
}
level1.children = level1.children.filter((level2) => {
if (!level2.meta.requiresMenu) {
return false
}
for (const menu_ of menus) {
if (_.startsWith(menu_.url, level1.path + '/' + level2.path)) {
hasChildren = true
return true
}
}
return false
})
return hasChildren
}
}
return false
} else {
let hasChildren = false
if (level1.children === undefined) {
return true
}
level1.children = level1.children.filter((level2) => {
if (!level2.meta.requiresMenu) {
return false
}
if (!level2.meta.requiresMenuAuth) {
hasChildren = true
return true
}
for (const menu_ of menus) {
if (_.startsWith(menu_.url, level1.path + '/' + level2.path)) {
hasChildren = true
return true
}
}
return false
})
return hasChildren
}
})
}
}
</script>
<style scoped>
.background {
height: 100vh;
min-height: 500px;
background: var(--background-color);
}
.aside {
border-right: 1px #ddd solid;
box-shadow: 0 0 1em #ddd;
}
.menu:not(.el-menu--collapse) {
width: 13vw;
min-width: 190px;
}
.menu-top > * {
padding: 0;
}
.menu-production-name {
width: 100%;
padding: 0 10px;
font-size: 1.2em;
color: var(--main-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.main-header {
display: flex;
align-items: center;
justify-content: right;
border-bottom: 1px #ddd solid;
box-shadow: 0 0 1em #ddd;
}
.main-header > *:not(:last-child) {
margin-right: 15px;
}
.user-head {
display: inline-block;
}
.user-head > span {
font-size: 0.8em;
}
.user-info {
margin-left: 10px;
}
.user-name {
color: var(--main-color);
font-size: 1.2em;
font-weight: bold;
}
.user-desc {
color: var(--font-secondary-color);
}
.noPadding {
padding: 0;
}
</style>

86
src/router/index.ts Normal file
View File

@@ -0,0 +1,86 @@
import { createRouter, createWebHistory } from 'vue-router'
import { PRODUCTION_NAME } from '@/constants/Common.constants'
import { getLoginStatus, getUser } from '@/utils/auth'
import _ from 'lodash'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: async () => await import('@/pages/Main.vue'),
children: [
{
path: '',
redirect: 'home'
},
{
path: '/home',
component: async () => await import('@/pages/Home.vue'),
name: 'home',
meta: {
title: '首页',
icon: shallowRef(IconFrameworkHome),
requiresMenu: true,
requiresScrollbar: false,
requiresPadding: true,
requiresAuth: false,
requiresMenuAuth: false
}
}
]
},
{
path: '/login',
component: async () => await import('@/pages/Login.vue'),
name: 'Login',
meta: {
title: '登录',
requiresAuth: false
}
}
]
})
router.beforeEach(async (to, from, next) => {
if (to.matched.length === 0) {
from.path !== '' ? next({ path: from.path }) : next('/')
} else {
if (to.meta.title !== '') {
document.title = `${PRODUCTION_NAME} - ${to.meta.title as string}`
}
if (to.meta.requiresAuth === true) {
if (getLoginStatus()) {
if (to.name === 'Login') {
next('/')
} else {
if (to.meta.requiresAuth) {
const user = await getUser()
const menus = user.menus
for (const menu of menus) {
if (menu.url === '/') continue
if (_.startsWith(to.path, menu.url)) {
next()
return
}
}
next('/')
} else {
next()
}
}
} else {
if (to.name === 'Login') {
next()
} else {
next('/login')
}
}
} else {
next()
}
}
})
export default router

148
src/services/index.ts Normal file
View File

@@ -0,0 +1,148 @@
import axios, { type AxiosError, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import jwtDecode from 'jwt-decode'
import { clearLocalStorage, getToken } from '@/utils/common'
import router from '@/router'
import {
ACCESS_DENIED,
DATABASE_DATA_TO_LONG,
DATABASE_DATA_VALIDATION_FAILED,
DATABASE_EXECUTE_ERROR,
TOKEN_HAS_EXPIRED,
TOKEN_IS_ILLEGAL,
UNAUTHORIZED
} from '@/constants/Common.constants'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: 'http://localhost:8621',
timeout: 10000,
withCredentials: false
})
service.defaults.paramsSerializer = (params) => {
return Object.keys(params)
.filter((it) => {
return Object.prototype.hasOwnProperty.call(params, it)
})
.reduce((pre, curr) => {
return params[curr] !== null
? (pre !== '' ? pre + '&' : '') + curr + '=' + encodeURIComponent(params[curr])
: pre
}, '')
}
service.interceptors.request.use(
async (config) => {
let token = getToken()
if (token != null) {
const jwt = jwtDecode(token)
if (
(jwt as any).exp * 1000 - new Date().getTime() < 1200000 &&
(jwt as any).exp * 1000 - new Date().getTime() > 0
) {
/*
await axios
.get('http://localhost:8621/token', {
headers: { token }
})
.then((res) => {
const response = res.data
if (response.code === TOKEN_RENEW_SUCCESS) {
setToken(response.data?.token ?? '')
}
})
*/
}
token = getToken()
config.headers.set('token', token)
}
return config
},
async (error) => {
return await Promise.reject(error)
}
)
service.interceptors.response.use(
async (response) => {
switch (response.data.code) {
case UNAUTHORIZED:
case TOKEN_IS_ILLEGAL:
case TOKEN_HAS_EXPIRED:
clearLocalStorage()
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>登录已过期</strong>'
})
setTimeout(function () {
router.go(0)
}, 1500)
throw response?.data
case ACCESS_DENIED:
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>暂无权限操作</strong>'
})
throw response?.data
case DATABASE_DATA_TO_LONG:
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>数据过长</strong>'
})
throw response?.data
case DATABASE_DATA_VALIDATION_FAILED:
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>数据验证失败</strong>'
})
throw response?.data
case DATABASE_EXECUTE_ERROR:
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>数据库执行出错</strong>'
})
throw response?.data
}
return response
},
async (error) => {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>服务器出错</strong>,请稍后重试'
})
return await Promise.reject(error?.response?.data)
}
)
const request = {
async get<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('GET', url, { params: data })
},
async post<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('POST', url, { data })
},
async put<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('PUT', url, { data })
},
async delete<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('DELETE', url, { params: data })
},
async request<T>(
method = 'GET',
url: string,
data?: AxiosRequestConfig
): Promise<AxiosResponse<_Response<T>>> {
return await new Promise((resolve, reject) => {
service({ method, url, ...data })
.then((res) => {
resolve(res as unknown as Promise<AxiosResponse<_Response<T>>>)
})
.catch((e: Error | AxiosError) => {
reject(e)
})
})
}
}
export default request

0
src/store/index.ts Normal file
View File

69
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,69 @@
import { clearLocalStorage, getCaptcha, getLocalStorage, setLocalStorage } from './common'
import { DATABASE_SELECT_OK, TOKEN_NAME } from '@/constants/Common.constants'
import request from '@/services'
import type { AxiosResponse } from 'axios'
let captcha: Captcha
export async function login(
username: string,
password: string
): Promise<AxiosResponse<_Response<Token>>> {
return await request.post<Token>('/login', {
username,
password
})
}
export function logout(): void {
request.get('/logout').finally(() => {
clearLocalStorage()
})
}
export function getLoginStatus(): boolean {
return getLocalStorage(TOKEN_NAME) !== null
}
export async function getUser(): Promise<User> {
if (getLocalStorage('userInfo') !== null) {
return await new Promise((resolve) => {
resolve(JSON.parse(getLocalStorage('userInfo') as string) as User)
})
}
return await requestUser()
}
export async function requestUser(): Promise<User> {
let user: User | null
await request.get<User>('/user/info').then((value) => {
const response = value.data
if (response.code === DATABASE_SELECT_OK) {
user = response.data
setLocalStorage('userInfo', JSON.stringify(user))
}
})
return await new Promise<User>((resolve, reject) => {
if (user != null) {
resolve(user)
}
reject(user)
})
}
export async function getUsername(): Promise<string> {
const user = await getUser()
return user.username
}
export function getCaptchaSrc(): string {
captcha = getCaptcha(300, 150, 4)
return captcha.base64Src
}
export function verifyCaptcha(value: string): boolean {
return captcha.value.toLowerCase() === value.replace(/\s*/g, '').toLowerCase()
}

136
src/utils/common.ts Normal file
View File

@@ -0,0 +1,136 @@
import { TOKEN_NAME } from '@/constants/Common.constants'
export function getQueryVariable(variable: string): string | null {
const query = window.location.search.substring(1)
const vars = query.split('&')
for (const value of vars) {
const pair = value.split('=')
if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, ' '))
}
}
return null
}
export function setCookie(
name: string,
value: string,
daysToLive: number | null,
path: string | null
): void {
let cookie = name + '=' + encodeURIComponent(value)
if (typeof daysToLive === 'number') {
cookie = `${cookie}; max-age=${daysToLive * 24 * 60 * 60}`
}
if (typeof path === 'string') {
cookie += '; path=' + path
}
document.cookie = cookie
}
export function setLocalStorage(name: string, value: string): void {
localStorage.setItem(name, value)
}
export function setToken(token: string): void {
setLocalStorage(TOKEN_NAME, token)
}
export function getCookie(name: string): string | null {
const cookieArr = document.cookie.split(';')
for (const cookie of cookieArr) {
const cookiePair = cookie.split('=')
if (cookiePair[0].trim() === name) {
return decodeURIComponent(cookiePair[1])
}
}
return null
}
export function getLocalStorage(name: string): string | null {
return localStorage.getItem(name)
}
export function getToken(): string | null {
return getLocalStorage(TOKEN_NAME)
}
export function removeCookie(name: string): void {
document.cookie = name + '=; max-age=0'
}
export function removeLocalStorage(name: string): void {
localStorage.removeItem(name)
}
export function removeToken(): void {
removeLocalStorage(TOKEN_NAME)
}
export function clearLocalStorage(): void {
localStorage.clear()
}
export function getCaptcha(width: number, high: number, num: number): Captcha {
const CHARTS = '23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijklmnpqrstuvwxyz'.split('')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.rect(0, 0, width, high)
ctx.clip()
ctx.fillStyle = randomColor(200, 250)
ctx.fillRect(0, 0, width, high)
for (let i = 0.05 * width * high; i > 0; i--) {
ctx.fillStyle = randomColor(0, 256)
ctx.fillRect(randomInt(0, width), randomInt(0, high), 1, 1)
}
ctx.font = `${high - 4}px Consolas`
ctx.fillStyle = randomColor(160, 200)
let value = ''
for (let i = 0; i < num; i++) {
const x = ((width - 10) / num) * i + 5
const y = high - 12
const r = Math.PI * randomFloat(-0.12, 0.12)
const ch = CHARTS[randomInt(0, CHARTS.length)]
value += ch
ctx.translate(x, y)
ctx.rotate(r)
ctx.fillText(ch, 0, 0)
ctx.rotate(-r)
ctx.translate(-x, -y)
}
const base64Src = canvas.toDataURL('image/jpg')
return {
value,
base64Src
}
}
function randomInt(start: number, end: number): number {
if (start > end) {
const t = start
start = end
end = t
}
start = Math.ceil(start)
end = Math.floor(end)
return start + Math.floor(Math.random() * (end - start))
}
function randomFloat(start: number, end: number): number {
return start + Math.random() * (end - start)
}
function randomColor(start: number, end: number): string {
return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})`
}

44
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface Captcha {
value: string
base64Src: string
}
interface RouteHandle {
auth: boolean
}
interface _Response<T> {
code: number
msg: string
data: T | null
}
interface Token {
token: string
}
interface User {
id: number
username: string
menus: Menu[]
enable: number
}
interface Menu {
url?: string
}
interface LoginForm {
username: string
password: string
}

12
tsconfig.app.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}

8
tsconfig.node.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

9
tsconfig.vitest.json Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.app.json",
"exclude": [],
"compilerOptions": {
"composite": true,
"lib": [],
"types": ["node", "jsdom"]
}
}

86
vite.config.ts Normal file
View File

@@ -0,0 +1,86 @@
import { fileURLToPath, URL } from 'node:url'
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import Icons from 'unplugin-icons/vite'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import Inspect from 'vite-plugin-inspect'
const pathSrc = path.resolve(__dirname, 'src')
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
// 目标文件
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/ // .md
],
// Auto import functions from Vue, e.g. ref, reactive, toRef...
// 自动导入 Vue 相关函数ref, reactive, toRef 等
imports: ['vue', 'vue-router'],
// eslint报错解决
eslintrc: {
enabled: false, // Default `false`
filepath: '.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
},
// Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style)
// 自动导入 Element Plus 相关函数ElMessage, ElMessageBox... (带样式)
resolvers: [
// Auto import icon components
// 自动导入图标组件
IconsResolver({
prefix: 'icon',
alias: {
system: 'system-uicons'
},
enabledCollections: ['ep'],
customCollections: ['framework']
}),
ElementPlusResolver()
],
dts: path.resolve(pathSrc, 'auto-imports.d.ts')
}),
Components({
resolvers: [
IconsResolver({
prefix: 'icon',
alias: {
system: 'system-uicons'
},
enabledCollections: ['ep'],
customCollections: ['framework']
}),
ElementPlusResolver()
],
dts: path.resolve(pathSrc, 'components.d.ts')
}),
Icons({
compiler: 'vue3',
autoInstall: true,
customCollections: {
framework: FileSystemIconLoader('src/assets/svg', (svg) =>
svg.replace(/^svg /, '<svg fill="currentColor"')
)
}
}),
Inspect()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

15
vitest.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig } from 'vite'
import { configDefaults, defineConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/*'],
root: fileURLToPath(new URL('./', import.meta.url))
}
})
)