拉取vue版本代码
@ -1,4 +1,4 @@
|
||||
# CURD-TS 正在开发中……
|
||||
# Vue3.0版本已开发完成!
|
||||
一套基于TS的增删改查系统,前端语言Vue3.0、React、Angular,后端语言node+express,采用了三种数据库MySQL、MongoDB、SQLite编写。
|
||||
A TS based add, delete, modify and query system, the front-end language vue3.0, react, angular, back-end language node + Express, using three kinds of database mysql, mongodb, SQLite
|
||||
|
||||
|
5
frontend/vue-ts/.env
Normal file
@ -0,0 +1,5 @@
|
||||
# public path
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# Cross-domain proxy, you can configure multiple
|
||||
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]
|
11
frontend/vue-ts/.env.development
Normal file
@ -0,0 +1,11 @@
|
||||
# port
|
||||
VITE_PORT = 3001
|
||||
|
||||
# open
|
||||
VITE_OPEN = false
|
||||
|
||||
# public path
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# Cross-domain proxy, you can configure multiple
|
||||
VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]
|
2
frontend/vue-ts/.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
# public path
|
||||
VITE_PUBLIC_PATH = /manages/
|
4
frontend/vue-ts/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/node_modules
|
||||
/dist
|
||||
.DS_Store
|
||||
src/.DS_Store
|
@ -1 +1,55 @@
|
||||
# vue-ts
|
||||
# Vue3.0后台管理系统
|
||||
|
||||
## 知识库地址
|
||||
|
||||
帮助你获取最新的 API
|
||||
[vue3.0 中文文档地址]: https://vue3js.cn/docs/zh/
|
||||
[element-plus 中文文档地址]: https://element-plus.org/#/zh-CN
|
||||
[composition-Api 中文文档地址]: https://composition-api.vuejs.org/zh/
|
||||
[vue-router-next 文档地址]: https://next.router.vuejs.org/
|
||||
[next.vuex 文档地址]: https://next.vuex.vuejs.org/
|
||||
[vite 源码]: https://github.com/vitejs/vite
|
||||
[vite 文档地址]: https://vitejs.dev/
|
||||
[vite 中文文档地址(非官方版本)]: https://vite-design.surge.sh/guide/chinese-doc.html
|
||||
[vue-i18n-next]: https://vue-i18n-next.intlify.dev/
|
||||
[composition-api-vue-i18n-next]: https://vue-i18n-next.intlify.dev/advanced/composition.html#local-scope
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## 项目运行
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## 项目打包
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 注意点
|
||||
|
||||
请先全局安装 typescript、ts-node、vite 如安装请忽略
|
||||
|
||||
```
|
||||
npm install -g typescript
|
||||
npm install -g ts-node
|
||||
npm install -g create-vite-app
|
||||
```
|
||||
|
||||
坑位
|
||||
1.
|
||||
path模块线上部署会遇到process is undefined问题
|
||||
解决办法:在源码中开头加入window.process = {}
|
||||
issues:https://github.com/jinder/path/issues/7
|
||||
2.
|
||||
运行项目时控制台报NODE_ENV not found
|
||||
解决办法:删除node_modules和package-lock.json文件,重新npm install
|
||||
3.
|
||||
运行项目会感觉菜单切换比较卡,这个原因是使用route造成的,watch(route)是隐式的{ deep: true },最好使用watchEffect
|
||||
issues:https://github.com/vuejs/vue-next/issues/2027
|
6
frontend/vue-ts/babel.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
const productPlugins = []
|
||||
process.env.NODE_ENV === "production" && productPlugins.push("transform-remove-console")
|
||||
module.exports = {
|
||||
plugins: [...productPlugins],
|
||||
}
|
19
frontend/vue-ts/build/proxy.ts
Normal file
@ -0,0 +1,19 @@
|
||||
type ProxyItem = [string, string];
|
||||
|
||||
type ProxyList = ProxyItem[];
|
||||
|
||||
const regExps = (value: string,reg: string): string => {
|
||||
return value.replace(new RegExp(reg, 'g'), '');
|
||||
}
|
||||
|
||||
export function createProxy(list: ProxyList = []) {
|
||||
const ret: any = {};
|
||||
for (const [prefix, target] of list) {
|
||||
ret[prefix] = {
|
||||
target: target,
|
||||
changeOrigin: true,
|
||||
rewrite: (path:string) => regExps(path, prefix)
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
}
|
38
frontend/vue-ts/build/utils.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
export interface ViteEnv {
|
||||
VITE_PORT: number;
|
||||
VITE_OPEN: boolean;
|
||||
VITE_USE_MOCK: boolean;
|
||||
VITE_PUBLIC_PATH: string;
|
||||
VITE_PROXY: [string, string][];
|
||||
}
|
||||
|
||||
export function loadEnv(): ViteEnv {
|
||||
const env = process.env.NODE_ENV;
|
||||
const ret: any = {};
|
||||
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,]
|
||||
envList.forEach((e) => {
|
||||
dotenv.config({
|
||||
path: e,
|
||||
});
|
||||
});
|
||||
for (const envName of Object.keys(process.env)) {
|
||||
let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
|
||||
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
|
||||
if (envName === 'VITE_PORT') {
|
||||
realName = Number(realName);
|
||||
}
|
||||
if (envName === 'VITE_OPEN') {
|
||||
realName = Boolean(realName);
|
||||
}
|
||||
if (envName === 'VITE_PROXY') {
|
||||
try {
|
||||
realName = JSON.parse(realName);
|
||||
} catch (error) { }
|
||||
}
|
||||
ret[envName] = realName;
|
||||
process.env[envName] = realName;
|
||||
}
|
||||
return ret;
|
||||
}
|
258
frontend/vue-ts/index.html
Normal file
@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/iconfont.css" />
|
||||
<link rel="stylesheet" href="/animate.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.chromeframe {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: -75px 0 0 -75px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
/* COLOR 1 */
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-ms-animation: spin 2s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-moz-animation: spin 2s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-o-animation: spin 2s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
animation: spin 2s linear infinite;
|
||||
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#loader:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
/* COLOR 2 */
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 3s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-moz-animation: spin 3s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-o-animation: spin 3s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-ms-animation: spin 3s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
animation: spin 3s linear infinite;
|
||||
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
|
||||
#loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
/* COLOR 3 */
|
||||
-moz-animation: spin 1.5s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-o-animation: spin 1.5s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-ms-animation: spin 1.5s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
-webkit-animation: spin 1.5s linear infinite;
|
||||
/* Chrome, Opera 15+, Safari 5+ */
|
||||
animation: spin 1.5s linear infinite;
|
||||
/* Chrome, Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: rotate(0deg);
|
||||
/* IE 9 */
|
||||
transform: rotate(0deg);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: rotate(360deg);
|
||||
/* IE 9 */
|
||||
transform: rotate(360deg);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: rotate(0deg);
|
||||
/* IE 9 */
|
||||
transform: rotate(0deg);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: rotate(360deg);
|
||||
/* IE 9 */
|
||||
transform: rotate(360deg);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 51%;
|
||||
height: 100%;
|
||||
background: #5d94f8;
|
||||
/* Old browsers */
|
||||
z-index: 1000;
|
||||
-webkit-transform: translateX(0);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: translateX(0);
|
||||
/* IE 9 */
|
||||
transform: translateX(0);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Loaded */
|
||||
.loaded #loader-wrapper .loader-section.section-left {
|
||||
-webkit-transform: translateX(-100%);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: translateX(-100%);
|
||||
/* IE 9 */
|
||||
transform: translateX(-100%);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-right {
|
||||
-webkit-transform: translateX(100%);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: translateX(100%);
|
||||
/* IE 9 */
|
||||
transform: translateX(100%);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader {
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper {
|
||||
visibility: hidden;
|
||||
-webkit-transform: translateY(-100%);
|
||||
/* Chrome, Opera 15+, Safari 3.1+ */
|
||||
-ms-transform: translateY(-100%);
|
||||
/* IE 9 */
|
||||
transform: translateY(-100%);
|
||||
/* Firefox 16+, IE 10+, Opera */
|
||||
-webkit-transition: all 0.3s 1s ease-out;
|
||||
transition: all 0.3s 1s ease-out;
|
||||
}
|
||||
|
||||
/* JavaScript Turned Off */
|
||||
.no-js #loader-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-js h1 {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: 'Open Sans';
|
||||
color: #FFF;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<div id="loader-wrapper">
|
||||
<div id="loader"></div>
|
||||
<div class="loader-section section-left"></div>
|
||||
<div class="loader-section section-right"></div>
|
||||
<div class="load_title">加载中,请耐心等待...
|
||||
<br>
|
||||
<span id="version">V0.0.1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1142
frontend/vue-ts/package-lock.json
generated
Normal file
41
frontend/vue-ts/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "vue-ts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"await-to-js": "^2.1.1",
|
||||
"axios": "^0.21.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"element-plus": "^1.0.2-beta.30",
|
||||
"mitt": "^2.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"screenfull": "^5.0.2",
|
||||
"v-contextmenu": "^3.0.0-alpha.4",
|
||||
"vue": "^3.0.4",
|
||||
"vue-i18n": "^9.0.0-rc.4",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuex": "^4.0.0-rc.2",
|
||||
"vxe-table": "^4.0.0-beta.3",
|
||||
"xe-utils": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.14",
|
||||
"@vitejs/plugin-vue": "^1.1.2",
|
||||
"@vue/compiler-sfc": "^3.0.4",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"postcss-import": "^12.0.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "^4.1.3",
|
||||
"vite": "^2.0.0-beta.56"
|
||||
}
|
||||
}
|
3
frontend/vue-ts/postcss.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require('autoprefixer'), require('postcss-import')],
|
||||
};
|
11
frontend/vue-ts/public/animate.css
vendored
Normal file
BIN
frontend/vue-ts/public/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
18
frontend/vue-ts/public/iconfont.css
Normal file
@ -0,0 +1,18 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* project id 1098500 */
|
||||
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot");
|
||||
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot?#iefix")
|
||||
format("embedded-opentype"),
|
||||
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff2") format("woff2"),
|
||||
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff") format("woff"),
|
||||
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.ttf") format("truetype"),
|
||||
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.svg#iconfont") format("svg");
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
3
frontend/vue-ts/src/App.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
16
frontend/vue-ts/src/api/user.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { http } from "../utils/http"
|
||||
|
||||
// 获取验证码
|
||||
export const getVerify = (): any => {
|
||||
return http.request("get", "/captcha")
|
||||
}
|
||||
|
||||
// 登录
|
||||
export const getLogin = (data: object): any => {
|
||||
return http.request("post", "/login", data)
|
||||
}
|
||||
|
||||
// 注册
|
||||
export const getRegist = (data: object): any => {
|
||||
return http.request("post", "/register", data)
|
||||
}
|
BIN
frontend/vue-ts/src/assets/401.gif
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
frontend/vue-ts/src/assets/404.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
frontend/vue-ts/src/assets/404_cloud.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
frontend/vue-ts/src/assets/bg.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/vue-ts/src/assets/ch.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/vue-ts/src/assets/en.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
25
frontend/vue-ts/src/assets/iconfont/iconfont.css
Normal file
@ -0,0 +1,25 @@
|
||||
@font-face {font-family: "iconfont";
|
||||
src: url('iconfont.eot?t=1607695324289'); /* IE9 */
|
||||
src: url('iconfont.eot?t=1607695324289#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOYAAsAAAAACDQAAANLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCfgqEFIMzATYCJAMMCwgABCAFhG0HRhsBB8gekiQBgDxM4A0kAIgioN8v3Ud3F2KVqgCpNrIc0GFVJBVVWVtFrBIhOx5JmMaWNar/v/v99nGtOz+KOqLjkL7t7ovvDWum0bJoImRoGiKExrYjC6g2lM6fV4PQ9vCs/c/l9Fp+RWXzA+Uyx9aoF2A0gQIaa2iUaIEk6CkMr13sMM8T0GwbFdns/NQS6pLRaQHhwtl8BnVldHIDMWgoaxVrs3BV0YiX4wdcCb4fftkeGkQloXMXToUp8n2d+DqZLlXbDngI/nSXQUHCFCQT1iptC1qj2JRWzVqszRC0rWGBr5NVxdcJj7fZPzyCSKihha1gDNJ4RXxH5tL4iIu49k3U0IwNvGUIvZ6i9FKZzM04f1vGCnHQizTFjAGXOnGWLmi8LgG9xC1WmK8lB4pOtBX0Kg4xYfE2ry5ey8Q6JbR6iyf7Mjpdw0qVKO8Uzh3pmbbum7XpTGXrUPUu9Y7qR9nckW8NQE7IjYWNjrm3/r2srSk+619wmEI3RohhGW4tZLwr/U/zycbfUpLCGCbLAW6wwYFl9c4IGxVz7/1lNW2pbcz0E4xYib5sbUw75r4GyvsNvC+bMxvPTkWYPDcepPTFWegO8LOcLO8GFpzGjRcnxHJeHkVhSpcJ8bkDkc+BJ8vxGUGffT10F1p7Vsv7gfWxseePRJdRGkL1OL1IM1D1JEWKPb/zN6bCpwOHB3f+brRk8Pn/w63AWz3Ouhs0btge+Hn8jjVZF1J9yirP4ZnJZFH76+GAIzpVqKvb33BFX+dCgoa+BKK6QUgahjCZMAVFi2l/TcPRCmk2KYrNLXpwrUJuwoQDAkGnV4jaPYak0w8mE36h6PcPNZ0hQbO10LNni5FwtnlmHTICKM5u4WKuXmO2UfSExQuQX6tkeFoYUAlwqRjEeo0un4xCDfgUC6T1vEEIhhmvV3GEXAaVSh03eL0EOaEpCNFwaLWs6kWaXL2KeszgQIYAKCxrC1aUU1fDvMZCT+HzC0DemooM3lBVYyUAJyn2julp6DpgorJap6pbGSxZl2cgCIydxHB1VVgEFlCxYnVYo3pUCcgRNAoDIg0OWqYe6yrTLK+ovt8uaEZ/l0IMKWQdJy8WhZqtVSpSjgPUemItQgA=') format('woff2'),
|
||||
url('iconfont.woff?t=1607695324289') format('woff'),
|
||||
url('iconfont.ttf?t=1607695324289') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
|
||||
url('iconfont.svg?t=1607695324289#iconfont') format('svg'); /* iOS 4.1- */
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.team-iconexit-fullscreen:before {
|
||||
content: "\e62a";
|
||||
}
|
||||
|
||||
.team-iconfullscreen:before {
|
||||
content: "\e62b";
|
||||
}
|
||||
|
BIN
frontend/vue-ts/src/assets/iconfont/iconfont.eot
Normal file
1
frontend/vue-ts/src/assets/iconfont/iconfont.js
Normal file
@ -0,0 +1 @@
|
||||
!function(e){var t,c,l,n,o,i,d='<svg><symbol id="team-iconexit-fullscreen" viewBox="0 0 1024 1024"><path d="M366.2 181.8c-1-8-10.8-11.4-16.5-5.7l-53.1 53.1L134.2 67c-3.8-3.8-10-3.8-13.7 0L69 118.3c-3.8 3.8-3.8 10 0 13.7l162.4 162.4-53.3 53.3c-5.7 5.7-2.3 15.5 5.7 16.5l194.6 23c6.2 0.7 11.5-4.5 10.8-10.8l-23-194.6z m12.3 453.3l-194.7 23c-8 1-11.4 10.8-5.7 16.5l53.3 53.3L69 890.1c-3.8 3.8-3.8 10 0 13.7l51.5 51.4c3.8 3.8 10 3.8 13.7 0l162.4-162.3 53.1 53.1c5.7 5.7 15.5 2.3 16.5-5.7l23-194.4c0.7-6.3-4.5-11.5-10.7-10.8z m269.4-248l194.7-23c8-1 11.4-10.8 5.7-16.5L795 294.4l162.4-162.3c3.8-3.8 3.8-10 0-13.7L905.9 67c-3.8-3.8-10-3.8-13.7 0L729.7 229.2l-53.1-53.1c-5.7-5.7-15.6-2.3-16.5 5.7l-23 194.5c-0.6 6.3 4.6 11.5 10.8 10.8zM795 727.8l53.3-53.3c5.7-5.7 2.3-15.5-5.7-16.5L648 635c-6.2-0.7-11.5 4.5-10.8 10.8l23 194.6c1 8 10.8 11.4 16.5 5.7l53.1-53.1 162.4 162.3c3.8 3.8 10 3.8 13.7 0l51.5-51.4c3.8-3.8 3.8-10 0-13.7L795 727.8z m0 0" fill="#515151" ></path></symbol><symbol id="team-iconfullscreen" viewBox="0 0 1024 1024"><path d="M229.8 163l55.7-55.7c6-6 2.4-16.2-6-17.2l-203.2-24c-6.5-0.8-12 4.7-11.3 11.3l24 203.2c1 8.4 11.3 11.9 17.2 6l55.4-55.4 169.6 169.4c3.9 3.9 10.4 3.9 14.3 0l53.8-53.6c3.9-3.9 3.9-10.4 0-14.3L229.8 163z m447.3 237.6c3.9 3.9 10.4 3.9 14.3 0L861 231.1l55.4 55.4c6 6 16.2 2.4 17.2-6l24-203c0.8-6.5-4.7-12-11.3-11.3l-203.2 24c-8.4 1-11.9 11.3-6 17.2l55.7 55.7-169.5 169.4c-3.9 3.9-3.9 10.4 0 14.3l53.8 53.8z m256.6 343.9c-1-8.4-11.3-11.9-17.2-6L861 794 691.4 624.5c-3.9-3.9-10.4-3.9-14.3 0l-53.8 53.6c-3.9 3.9-3.9 10.4 0 14.3L792.9 862l-55.7 55.7c-6 6-2.4 16.2 6 17.2l203.2 24c6.5 0.8 12-4.7 11.3-11.3l-24-203.1z m-588.1-120c-3.9-3.9-10.4-3.9-14.3 0L161.7 794l-55.4-55.4c-6-6-16.2-2.4-17.2 6l-24 203c-0.8 6.5 4.7 12.1 11.3 11.3l203.2-24c8.4-1 11.9-11.3 6-17.2l-55.7-55.5 169.6-169.4c3.9-3.9 3.9-10.4 0-14.3l-53.9-54z m0 0" fill="#515151" ></path></symbol></svg>',s=(s=document.getElementsByTagName("script"))[s.length-1].getAttribute("data-injectcss");if(s&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(e){console&&console.log(e)}}function a(){o||(o=!0,l())}t=function(){var e,t,c,l;(l=document.createElement("div")).innerHTML=d,d=null,(c=l.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",e=c,(t=document.body).firstChild?(l=e,(c=t.firstChild).parentNode.insertBefore(l,c)):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(c=function(){document.removeEventListener("DOMContentLoaded",c,!1),t()},document.addEventListener("DOMContentLoaded",c,!1)):document.attachEvent&&(l=t,n=e.document,o=!1,(i=function(){try{n.documentElement.doScroll("left")}catch(e){return void setTimeout(i,50)}a()})(),n.onreadystatechange=function(){"complete"==n.readyState&&(n.onreadystatechange=null,a())})}(window);
|
23
frontend/vue-ts/src/assets/iconfont/iconfont.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "2208059",
|
||||
"name": "CURD-TS",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "team-icon",
|
||||
"description": "增删查改xi't",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "5698509",
|
||||
"name": "全屏缩小",
|
||||
"font_class": "exit-fullscreen",
|
||||
"unicode": "e62a",
|
||||
"unicode_decimal": 58922
|
||||
},
|
||||
{
|
||||
"icon_id": "5698510",
|
||||
"name": "全屏显示",
|
||||
"font_class": "fullscreen",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
}
|
||||
]
|
||||
}
|
32
frontend/vue-ts/src/assets/iconfont/iconfont.svg
Normal file
@ -0,0 +1,32 @@
|
||||
<?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" >
|
||||
<!--
|
||||
2013-9-30: Created.
|
||||
-->
|
||||
<svg>
|
||||
<metadata>
|
||||
Created by iconfont
|
||||
</metadata>
|
||||
<defs>
|
||||
|
||||
<font id="iconfont" horiz-adv-x="1024" >
|
||||
<font-face
|
||||
font-family="iconfont"
|
||||
font-weight="500"
|
||||
font-stretch="normal"
|
||||
units-per-em="1024"
|
||||
ascent="896"
|
||||
descent="-128"
|
||||
/>
|
||||
<missing-glyph />
|
||||
|
||||
<glyph glyph-name="exit-fullscreen" unicode="" d="M366.2 714.2c-1 8-10.8 11.4-16.5 5.7l-53.1-53.1L134.2 829c-3.8 3.8-10 3.8-13.7 0L69 777.7c-3.8-3.8-3.8-10 0-13.7l162.4-162.4-53.3-53.3c-5.7-5.7-2.3-15.5 5.7-16.5l194.6-23c6.2-0.7 11.5 4.5 10.8 10.8l-23 194.6z m12.3-453.3l-194.7-23c-8-1-11.4-10.8-5.7-16.5l53.3-53.3L69 5.899999999999977c-3.8-3.8-3.8-10 0-13.7l51.5-51.4c3.8-3.8 10-3.8 13.7 0l162.4 162.3 53.1-53.1c5.7-5.7 15.5-2.3 16.5 5.7l23 194.4c0.7 6.3-4.5 11.5-10.7 10.8z m269.4 248l194.7 23c8 1 11.4 10.8 5.7 16.5L795 601.6l162.4 162.3c3.8 3.8 3.8 10 0 13.7L905.9 829c-3.8 3.8-10 3.8-13.7 0L729.7 666.8l-53.1 53.1c-5.7 5.7-15.6 2.3-16.5-5.7l-23-194.5c-0.6-6.3 4.6-11.5 10.8-10.8zM795 168.20000000000005l53.3 53.3c5.7 5.7 2.3 15.5-5.7 16.5L648 261c-6.2 0.7-11.5-4.5-10.8-10.8l23-194.6c1-8 10.8-11.4 16.5-5.7l53.1 53.1 162.4-162.3c3.8-3.8 10-3.8 13.7 0l51.5 51.4c3.8 3.8 3.8 10 0 13.7L795 168.20000000000005z m0 0" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="fullscreen" unicode="" d="M229.8 733l55.7 55.7c6 6 2.4 16.2-6 17.2l-203.2 24c-6.5 0.8-12-4.7-11.3-11.3l24-203.2c1-8.4 11.3-11.9 17.2-6l55.4 55.4 169.6-169.4c3.9-3.9 10.4-3.9 14.3 0l53.8 53.6c3.9 3.9 3.9 10.4 0 14.3L229.8 733z m447.3-237.6c3.9-3.9 10.4-3.9 14.3 0L861 664.9l55.4-55.4c6-6 16.2-2.4 17.2 6l24 203c0.8 6.5-4.7 12-11.3 11.3l-203.2-24c-8.4-1-11.9-11.3-6-17.2l55.7-55.7-169.5-169.4c-3.9-3.9-3.9-10.4 0-14.3l53.8-53.8z m256.6-343.9c-1 8.4-11.3 11.9-17.2 6L861 102 691.4 271.5c-3.9 3.9-10.4 3.9-14.3 0l-53.8-53.6c-3.9-3.9-3.9-10.4 0-14.3L792.9 34l-55.7-55.7c-6-6-2.4-16.2 6-17.2l203.2-24c6.5-0.8 12 4.7 11.3 11.3l-24 203.1z m-588.1 120c-3.9 3.9-10.4 3.9-14.3 0L161.7 102l-55.4 55.4c-6 6-16.2 2.4-17.2-6l-24-203c-0.8-6.5 4.7-12.1 11.3-11.3l203.2 24c8.4 1 11.9 11.3 6 17.2l-55.7 55.5 169.6 169.4c3.9 3.9 3.9 10.4 0 14.3l-53.9 54z m0 0" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
|
||||
|
||||
</font>
|
||||
</defs></svg>
|
After Width: | Height: | Size: 2.3 KiB |
BIN
frontend/vue-ts/src/assets/iconfont/iconfont.ttf
Normal file
BIN
frontend/vue-ts/src/assets/iconfont/iconfont.woff
Normal file
BIN
frontend/vue-ts/src/assets/iconfont/iconfont.woff2
Normal file
BIN
frontend/vue-ts/src/assets/login.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
frontend/vue-ts/src/assets/welcome - 副本.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
frontend/vue-ts/src/assets/welcome.png
Normal file
After Width: | Height: | Size: 127 KiB |
95
frontend/vue-ts/src/components/breadCrumb/index.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group appear name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||
<span
|
||||
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
|
||||
class="no-redirect"
|
||||
>{{ $t(item.meta.title) }}</span
|
||||
>
|
||||
<a v-else @click.prevent="handleLink(item)">{{
|
||||
$t(item.meta.title)
|
||||
}}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import * as pathToRegexp from "path-to-regexp";
|
||||
import { ref, defineComponent, watch, Ref } from "vue";
|
||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "breadCrumb",
|
||||
setup() {
|
||||
const levelList: Ref<RouteLocationMatched[]> = ref([]);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isDashboard = (route: RouteLocationMatched): Boolean | string => {
|
||||
const name = route && (route.name as string);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
|
||||
};
|
||||
|
||||
const getBreadcrumb = (): void => {
|
||||
let matched = route.matched.filter(
|
||||
(item) => item.meta && item.meta.title
|
||||
);
|
||||
const first = matched[0];
|
||||
if (!isDashboard(first)) {
|
||||
matched = [
|
||||
({
|
||||
path: "/welcome",
|
||||
meta: { title: "home" },
|
||||
} as unknown) as RouteLocationMatched,
|
||||
].concat(matched);
|
||||
}
|
||||
levelList.value = matched.filter(
|
||||
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||
);
|
||||
};
|
||||
|
||||
getBreadcrumb();
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => getBreadcrumb()
|
||||
);
|
||||
|
||||
const pathCompile = (path: string): string | Object => {
|
||||
const { params } = route;
|
||||
var toPath = pathToRegexp.compile(path);
|
||||
return toPath(params);
|
||||
};
|
||||
|
||||
const handleLink = (item: RouteLocationMatched): any => {
|
||||
const { redirect, path } = item;
|
||||
if (redirect) {
|
||||
router.push(redirect.toString());
|
||||
return;
|
||||
}
|
||||
router.push(pathCompile(path));
|
||||
};
|
||||
|
||||
return { levelList, handleLink };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped >
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
50
frontend/vue-ts/src/components/hamBurger/index.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div style="padding: 0 15px" @click="toggleClick">
|
||||
<svg
|
||||
:class="{ 'is-active': isActive }"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
>
|
||||
<path
|
||||
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
name: "hamBurger",
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["toggleClick"],
|
||||
setup(props, ctx) {
|
||||
const toggleClick = () => {
|
||||
ctx.emit("toggleClick");
|
||||
};
|
||||
|
||||
return { toggleClick };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.hamburger.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
207
frontend/vue-ts/src/components/info/index.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="info">
|
||||
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" class="rule-form">
|
||||
<el-form-item prop="userName">
|
||||
<el-input
|
||||
clearable
|
||||
v-model="ruleForm.userName"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="el-icon-user"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="passWord">
|
||||
<el-input
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
v-model="ruleForm.passWord"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="el-icon-lock"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="verify">
|
||||
<el-input
|
||||
maxlength="2"
|
||||
onkeyup="this.value=this.value.replace(/[^\d.]/g,'');"
|
||||
v-model.number="ruleForm.verify"
|
||||
placeholder="请输入验证码"
|
||||
></el-input>
|
||||
<span
|
||||
class="verify"
|
||||
title="刷新"
|
||||
v-html="ruleForm.svg"
|
||||
@click.prevent="refreshVerify"
|
||||
></span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click.prevent="onBehavior">
|
||||
{{ tipsFalse }}
|
||||
</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<span class="tips" @click="changPage">{{ tips }}</span>
|
||||
</el-form-item>
|
||||
<span title="测试用户 直接登录" class="secret" @click="noSecret"
|
||||
>免密登录</span
|
||||
>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
PropType,
|
||||
onBeforeMount,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { storageSession } from "../../utils/storage";
|
||||
|
||||
export interface ContextProps {
|
||||
userName: string;
|
||||
passWord: string;
|
||||
verify: number | null;
|
||||
svg: any;
|
||||
telephone?: number;
|
||||
dynamicText?: string;
|
||||
}
|
||||
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ruleForm: {
|
||||
type: Object as PropType<ContextProps>,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
emits: ["onBehavior", "refreshVerify"],
|
||||
setup(props, ctx) {
|
||||
let vm: any;
|
||||
|
||||
let tips = ref("注册");
|
||||
let tipsFalse = ref("登录");
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
watch(
|
||||
route,
|
||||
async ({ path }, prevRoute: unknown): Promise<void> => {
|
||||
await nextTick();
|
||||
path.includes("register")
|
||||
? (tips.value = "登录") && (tipsFalse.value = "注册")
|
||||
: (tips.value = "注册") && (tipsFalse.value = "登录");
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const rules: Object = ref({
|
||||
userName: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
passWord: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ min: 6, message: "密码长度必须不小于6位", trigger: "blur" },
|
||||
],
|
||||
verify: [
|
||||
{ required: true, message: "请输入验证码", trigger: "blur" },
|
||||
{ type: "number", message: "验证码必须是数字类型", trigger: "blur" },
|
||||
],
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
vm = getCurrentInstance(); //获取组件实例
|
||||
});
|
||||
|
||||
// 点击登录或注册
|
||||
const onBehavior = (evt: Object): void => {
|
||||
vm.refs.ruleForm.validate((valid: Boolean) => {
|
||||
if (valid) {
|
||||
ctx.emit("onBehavior", evt);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 刷新验证码
|
||||
const refreshVerify = (): void => {
|
||||
ctx.emit("refreshVerify");
|
||||
};
|
||||
|
||||
// 表单重置
|
||||
const resetForm = (): void => {
|
||||
vm.refs.ruleForm.resetFields();
|
||||
};
|
||||
|
||||
// 登录、注册页面切换
|
||||
const changPage = (): void => {
|
||||
tips.value === "注册" ? router.push("/register") : router.push("/login");
|
||||
};
|
||||
|
||||
const noSecret = (): void => {
|
||||
storageSession.setItem("info", {
|
||||
username: "测试用户",
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.test",
|
||||
});
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
return {
|
||||
rules,
|
||||
tips,
|
||||
tipsFalse,
|
||||
resetForm,
|
||||
onBehavior,
|
||||
refreshVerify,
|
||||
changPage,
|
||||
noSecret,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info {
|
||||
width: 30vw;
|
||||
height: 48vh;
|
||||
background: url("../../assets/login.png") no-repeat center;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
border-radius: 20px;
|
||||
right: 100px;
|
||||
top: 30vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@media screen and (max-width: 750px){
|
||||
width: 88vw;
|
||||
right: 25px;
|
||||
top: 22vh;
|
||||
}
|
||||
.rule-form {
|
||||
width: 80%;
|
||||
.verify {
|
||||
position: absolute;
|
||||
margin: -10px 0 0 -120px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
color: #409eff;
|
||||
float: right;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
.secret {
|
||||
color: #409eff;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
216
frontend/vue-ts/src/components/splitPane/index.vue
Normal file
@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div
|
||||
:style="{ cursor, userSelect }"
|
||||
class="vue-splitter-container clearfix"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<div
|
||||
:class="leftClass"
|
||||
:split="splitSet.split"
|
||||
:style="{ [type]: percent + '%' }"
|
||||
>
|
||||
<slot name="paneL"></slot>
|
||||
</div>
|
||||
|
||||
<resizer
|
||||
:style="{ [resizeType]: percent + '%' }"
|
||||
:split="splitSet.split"
|
||||
@mousedown.prevent="onMouseDown"
|
||||
@click.prevent="onClick"
|
||||
></resizer>
|
||||
|
||||
<div
|
||||
:class="rightClass"
|
||||
:split="splitSet.split"
|
||||
:style="{ [type]: 100 - percent + '%' }"
|
||||
>
|
||||
<slot name="paneR"></slot>
|
||||
</div>
|
||||
|
||||
<div v-if="active" class="vue-splitter-container-mask"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
getCurrentInstance,
|
||||
computed,
|
||||
watch,
|
||||
PropType,
|
||||
onBeforeMount,
|
||||
} from "vue";
|
||||
import resizer from "./resizer.vue";
|
||||
|
||||
export interface ContextProps {
|
||||
minPercent: number;
|
||||
defaultPercent: number;
|
||||
split: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "splitPane",
|
||||
components: { resizer },
|
||||
props: {
|
||||
splitSet: {
|
||||
type: Object as PropType<ContextProps>,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
emits: ["resize"],
|
||||
setup(props, ctx) {
|
||||
let active = ref(false);
|
||||
let hasMoved = ref(false);
|
||||
let height = ref(null);
|
||||
let percent = ref(props.splitSet?.defaultPercent);
|
||||
let type = props.splitSet?.split === "vertical" ? "width" : "height";
|
||||
let resizeType = props.splitSet?.split === "vertical" ? "left" : "top";
|
||||
|
||||
let leftClass = ref([
|
||||
"splitter-pane splitter-paneL",
|
||||
props.splitSet?.split,
|
||||
]);
|
||||
|
||||
let rightClass = ref([
|
||||
"splitter-pane splitter-paneR",
|
||||
props.splitSet?.split,
|
||||
]);
|
||||
|
||||
const userSelect = computed(() => {
|
||||
return active.value ? "none" : "";
|
||||
});
|
||||
|
||||
const cursor = computed(() => {
|
||||
return active.value
|
||||
? props.splitSet?.split === "vertical"
|
||||
? "col-resize"
|
||||
: "row-resize"
|
||||
: "";
|
||||
});
|
||||
|
||||
const onClick = (): void => {
|
||||
if (!hasMoved.value) {
|
||||
percent.value = 50;
|
||||
ctx.emit("resize", percent.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (): void => {
|
||||
active.value = true;
|
||||
hasMoved.value = false;
|
||||
};
|
||||
|
||||
const onMouseUp = (): void => {
|
||||
active.value = false;
|
||||
};
|
||||
|
||||
const onMouseMove = (e: any): void => {
|
||||
if (e.buttons === 0 || e.which === 0) {
|
||||
active.value = false;
|
||||
}
|
||||
|
||||
if (active.value) {
|
||||
let offset = 0;
|
||||
let target = e.currentTarget;
|
||||
if (props.splitSet?.split === "vertical") {
|
||||
while (target) {
|
||||
offset += target.offsetLeft;
|
||||
target = target.offsetParent;
|
||||
}
|
||||
} else {
|
||||
while (target) {
|
||||
offset += target.offsetTop;
|
||||
target = target.offsetParent;
|
||||
}
|
||||
}
|
||||
|
||||
const currentPage =
|
||||
props.splitSet?.split === "vertical" ? e.pageX : e.pageY;
|
||||
const targetOffset =
|
||||
props.splitSet?.split === "vertical"
|
||||
? e.currentTarget.offsetWidth
|
||||
: e.currentTarget.offsetHeight;
|
||||
const percents =
|
||||
Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100;
|
||||
|
||||
if (
|
||||
percents > props.splitSet?.minPercent &&
|
||||
percents < 100 - props.splitSet?.minPercent
|
||||
) {
|
||||
percent.value = percents;
|
||||
}
|
||||
|
||||
ctx.emit("resize", percent.value);
|
||||
|
||||
hasMoved.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
userSelect,
|
||||
cursor,
|
||||
active,
|
||||
hasMoved,
|
||||
height,
|
||||
percent,
|
||||
type,
|
||||
resizeType,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onMouseMove,
|
||||
leftClass: leftClass.value.join(" "),
|
||||
rightClass: rightClass.value.join(" "),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.vue-splitter-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.vue-splitter-container-mask {
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.splitter-pane.vertical.splitter-paneL {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
padding-right: 3px;
|
||||
}
|
||||
.splitter-pane.vertical.splitter-paneR {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneL {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
.splitter-pane.horizontal.splitter-paneR {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
59
frontend/vue-ts/src/components/splitPane/resizer.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="classes"></div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import { computed, defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
name: "resizer",
|
||||
props: {
|
||||
split: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
let classes = computed(() => {
|
||||
return ["splitter-pane-resizer", props.split, props.className].join(" ");
|
||||
});
|
||||
return {
|
||||
classes,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splitter-pane-resizer {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
position: absolute;
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.splitter-pane-resizer.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
.splitter-pane-resizer.vertical {
|
||||
width: 11px;
|
||||
height: 100%;
|
||||
margin-left: -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
</style>
|
46
frontend/vue-ts/src/layout/components/AppMain.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view :key="key" v-slot="{ Component }">
|
||||
<transition appear name="fade-transform" mode="out-in">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, defineComponent } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
export default defineComponent({
|
||||
name: "AppMain",
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const key = computed(() => route.path);
|
||||
|
||||
return { key };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-main {
|
||||
min-height: calc(100vh - 50px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 10px;
|
||||
}
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
178
frontend/vue-ts/src/layout/components/Navbar.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger
|
||||
:is-active="sidebar.opened"
|
||||
class="hamburger-container"
|
||||
@toggleClick="toggleSideBar"
|
||||
/>
|
||||
|
||||
<breadcrumb class="breadcrumb-container" />
|
||||
|
||||
<div class="right-menu">
|
||||
<screenfull />
|
||||
<div class="inter" :title="langs ? '中文' : '英文'" @click="toggleLang">
|
||||
<img :src="langs ? ch : en" />
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
<img :src="favicon" />
|
||||
<p>{{ usename }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="el-icon-switch-button" @click="logout">
|
||||
{{ $t("LoginOut") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, reactive, defineComponent, onMounted, nextTick } from "vue";
|
||||
import Breadcrumb from "../../components/breadCrumb/index.vue";
|
||||
import Hamburger from "../../components/hamBurger/index.vue";
|
||||
import screenfull from "../components/screenfull/index.vue";
|
||||
import { useMapGetters } from "../store";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { mapGetters, useStore } from "vuex";
|
||||
import { storageSession } from "../../utils/storage";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ch from "/@/assets/ch.png";
|
||||
import en from "/@/assets/en.png";
|
||||
import favicon from "/favicon.ico";
|
||||
export default defineComponent({
|
||||
name: "Navbar",
|
||||
components: {
|
||||
Breadcrumb,
|
||||
Hamburger,
|
||||
screenfull,
|
||||
},
|
||||
setup() {
|
||||
let langs = ref(true);
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
let usename = storageSession.getItem("info").username;
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
// 国际化语言切换
|
||||
const toggleLang = (): void => {
|
||||
langs.value = !langs.value;
|
||||
langs.value ? (locale.value = "ch") : (locale.value = "en");
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const logout = (): void => {
|
||||
storageSession.removeItem("info");
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document
|
||||
.querySelector(".el-dropdown__popper")
|
||||
?.setAttribute("class", "resetTop");
|
||||
document
|
||||
.querySelector(".el-popper__arrow")
|
||||
?.setAttribute("class", "hidden");
|
||||
});
|
||||
|
||||
return {
|
||||
// @ts-ignore
|
||||
...useMapGetters(["sidebar"]),
|
||||
toggleSideBar() {
|
||||
store.dispatch("app/toggleSideBar");
|
||||
},
|
||||
langs,
|
||||
usename,
|
||||
toggleLang,
|
||||
logout,
|
||||
ch,
|
||||
en,
|
||||
favicon
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.inter {
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-right: 5px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
img {
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
.el-dropdown-link {
|
||||
width: 80px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-right: 20px;
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// single element-plus reset
|
||||
.el-dropdown-menu__item {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.el-dropdown-menu {
|
||||
padding: 0;
|
||||
}
|
||||
.el-dropdown-menu__item:focus,
|
||||
.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
color: #606266;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
</style>
|
4
frontend/vue-ts/src/layout/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as Navbar } from './Navbar.vue'
|
||||
export { default as Sidebar } from './sidebar/index.vue'
|
||||
export { default as AppMain } from './AppMain.vue'
|
||||
export { default as setting } from './setting/index.vue'
|
133
frontend/vue-ts/src/layout/components/panel/index.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div ref="right-panel" :class="{ show: show }" class="right-panel-container">
|
||||
<div class="right-panel-background" />
|
||||
<div class="right-panel">
|
||||
<div
|
||||
class="handle-button"
|
||||
:title="show ? '关闭设置' : '打开设置'"
|
||||
@click="show = !show"
|
||||
>
|
||||
<i :class="show ? 'el-icon-close' : 'el-icon-setting'" />
|
||||
</div>
|
||||
<div class="right-panel-items">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import { addClass, removeClass } from "../../../utils/operate";
|
||||
import { ref, watch, getCurrentInstance, onMounted, onBeforeMount } from "vue";
|
||||
export default {
|
||||
name: "panel",
|
||||
setup() {
|
||||
let vm: any;
|
||||
|
||||
let show = ref(false);
|
||||
|
||||
watch(
|
||||
show,
|
||||
(val, prevVal) => {
|
||||
val ? addEventClick() : () => {};
|
||||
if (val) {
|
||||
addClass(document.body, "showright-panel");
|
||||
} else {
|
||||
removeClass(document.body, "showright-panel");
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const addEventClick = (): void => {
|
||||
window.addEventListener("click", closeSidebar);
|
||||
};
|
||||
|
||||
const closeSidebar = (evt: any): void => {
|
||||
const parent = evt.target.closest(".right-panel");
|
||||
if (!parent) {
|
||||
show.value = false;
|
||||
window.removeEventListener("click", closeSidebar);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
vm = getCurrentInstance();
|
||||
});
|
||||
|
||||
return {
|
||||
show,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.showright-panel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.right-panel-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 100%;
|
||||
max-width: 260px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
transform: translate(100%);
|
||||
background: #fff;
|
||||
z-index: 40000;
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
|
||||
|
||||
.right-panel-background {
|
||||
z-index: 20000;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.handle-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
left: -48px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
border-radius: 6px 0 0 6px !important;
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 48px;
|
||||
top: 45%;
|
||||
background: rgb(24, 144, 255);
|
||||
i {
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
77
frontend/vue-ts/src/layout/components/screenfull/index.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="screen-full" @click="onClick">
|
||||
<i
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
:class="
|
||||
isFullscreen
|
||||
? 'iconfont team-iconexit-fullscreen'
|
||||
: 'iconfont team-iconfullscreen'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import screenfull from "screenfull";
|
||||
import {
|
||||
ref,
|
||||
onBeforeMount,
|
||||
onUnmounted,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
} from "vue";
|
||||
export default defineComponent({
|
||||
name: "screenfull",
|
||||
setup() {
|
||||
let isFullscreen = ref(false);
|
||||
|
||||
const onClick = () => {
|
||||
if (!screenfull.isEnabled) return;
|
||||
screenfull.toggle();
|
||||
};
|
||||
|
||||
const change = () => {
|
||||
isFullscreen.value = screenfull.isFullscreen;
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on("change", change);
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off("change", change);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroy();
|
||||
});
|
||||
|
||||
return {
|
||||
isFullscreen,
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screen-full {
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
74
frontend/vue-ts/src/layout/components/setting/index.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<panel>
|
||||
<el-divider>界面显示</el-divider>
|
||||
<ul class="setting">
|
||||
<li>
|
||||
<span>灰色模式</span>
|
||||
<vxe-switch
|
||||
v-model="greyVal"
|
||||
open-label="开"
|
||||
close-label="关"
|
||||
@change="greyChange"
|
||||
></vxe-switch>
|
||||
</li>
|
||||
</ul>
|
||||
</panel>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import panel from "../panel/index.vue";
|
||||
import { onMounted, reactive, toRefs } from "vue";
|
||||
import { storageLocal } from "../../../utils/storage";
|
||||
export default {
|
||||
name: "setting",
|
||||
components: { panel },
|
||||
setup() {
|
||||
const localOperate = (key: string, value?: any, model?: string): any => {
|
||||
model && model === "set"
|
||||
? storageLocal.setItem(key, value)
|
||||
: storageLocal.getItem(key);
|
||||
};
|
||||
|
||||
const settings = reactive({
|
||||
greyVal: storageLocal.getItem("greyVal"),
|
||||
});
|
||||
|
||||
settings.greyVal === null
|
||||
? localOperate("greyVal", false, "set")
|
||||
: document.querySelector("html")?.setAttribute("class", "html-grey");
|
||||
|
||||
// 灰色模式设置
|
||||
const greyChange = ({ value }): void => {
|
||||
if (value) {
|
||||
localOperate("greyVal", true, "set");
|
||||
document.querySelector("html")?.setAttribute("class", "html-grey");
|
||||
} else {
|
||||
localOperate("greyVal", false, "set");
|
||||
document.querySelector("html")?.removeAttribute("class");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(settings),
|
||||
localOperate,
|
||||
greyChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting {
|
||||
width: 100%;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.el-divider__text) {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
30
frontend/vue-ts/src/layout/components/sidebar/Link.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Link",
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const linkProps = (to) => {
|
||||
return {
|
||||
to: to,
|
||||
};
|
||||
};
|
||||
return {
|
||||
type: "router-link",
|
||||
linkProps,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
101
frontend/vue-ts/src/layout/components/sidebar/SidebarItem.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template
|
||||
v-if="
|
||||
hasOneShowingChild(item.children, item) &&
|
||||
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
|
||||
!item.alwaysShow
|
||||
"
|
||||
>
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item
|
||||
:index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||
>
|
||||
<i :class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
|
||||
<template #title>
|
||||
<span>{{ $t(onlyOneChild.meta.title) }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<el-submenu
|
||||
v-else
|
||||
ref="subMenu"
|
||||
:index="resolvePath(item.path)"
|
||||
popper-append-to-body
|
||||
>
|
||||
<template #title>
|
||||
<i :class="item.meta.icon"></i>
|
||||
<span>{{ $t(item.meta.title) }}</span>
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import path from "path";
|
||||
import AppLink from "./Link.vue";
|
||||
import { defineComponent, PropType, ref } from "vue";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
export default defineComponent({
|
||||
name: "SidebarItem",
|
||||
components: { AppLink },
|
||||
props: {
|
||||
item: {
|
||||
type: Object as PropType<RouteRecordRaw>,
|
||||
required: true,
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const onlyOneChild = ref<RouteRecordRaw>({} as any);
|
||||
|
||||
function hasOneShowingChild(
|
||||
children: RouteRecordRaw[] = [],
|
||||
parent: RouteRecordRaw
|
||||
) {
|
||||
const showingChildren = children.filter((item) => {
|
||||
if (item.hidden) {
|
||||
return false;
|
||||
} else {
|
||||
onlyOneChild.value = item;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (showingChildren.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (showingChildren.length === 0) {
|
||||
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvePath = (routePath: string) => {
|
||||
return path.resolve(props.basePath, routePath);
|
||||
};
|
||||
|
||||
return { hasOneShowingChild, resolvePath, onlyOneChild };
|
||||
},
|
||||
});
|
||||
</script>
|
55
frontend/vue-ts/src/layout/components/sidebar/index.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
:unique-opened="false"
|
||||
active-text-color="#409EFF"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routes"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import SidebarItem from "./SidebarItem.vue";
|
||||
import { algorithm } from "../../../utils/algorithm";
|
||||
|
||||
export default defineComponent({
|
||||
name: "sidebar",
|
||||
components: { SidebarItem },
|
||||
setup() {
|
||||
const router = useRouter().options.routes;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route;
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
|
||||
return {
|
||||
routes: computed(() => algorithm.increaseIndexes(router)),
|
||||
activeMenu,
|
||||
isCollapse: computed(() => !store.getters.sidebar.opened),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
51
frontend/vue-ts/src/layout/components/tag/index.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="tags">
|
||||
<el-tag
|
||||
size="medium"
|
||||
v-for="tag in tags"
|
||||
:key="tag.name"
|
||||
closable
|
||||
:type="tag.type"
|
||||
>{{ tag.name }}</el-tag
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import { ref, defineComponent, onUnmounted, onMounted } from "vue";
|
||||
export default defineComponent({
|
||||
name: "tag",
|
||||
setup() {
|
||||
let flag = ref(true);
|
||||
|
||||
const tags = ref([
|
||||
{ name: "首页", type: "info" },
|
||||
{ name: "基础管理", type: "info" },
|
||||
]);
|
||||
|
||||
return {
|
||||
tags,
|
||||
flag,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags {
|
||||
height: 32px;
|
||||
float: right;
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: 0.18s;
|
||||
}
|
||||
:deep(.el-tag) {
|
||||
background-color: #fff;
|
||||
border: 1px solid #d0d7e7;
|
||||
margin-left: 4px;
|
||||
&:first-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
176
frontend/vue-ts/src/layout/index.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div :class="classes" class="app-wrapper">
|
||||
<div
|
||||
v-if="device === 'mobile' && sidebar.opened"
|
||||
class="drawer-bg"
|
||||
@click="handleClickOutside"
|
||||
/>
|
||||
<!-- 侧边栏 -->
|
||||
<sidebar class="sidebar-container" />
|
||||
<div class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<!-- 顶部导航栏 -->
|
||||
<navbar />
|
||||
</div>
|
||||
<!-- 主体内容 -->
|
||||
<app-main />
|
||||
</div>
|
||||
<!-- 系统设置 -->
|
||||
<setting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Navbar, Sidebar, AppMain, setting } from "./components";
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
toRefs,
|
||||
watch,
|
||||
watchEffect,
|
||||
onMounted,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
} from "vue";
|
||||
import { useStore } from "vuex";
|
||||
interface setInter {
|
||||
sidebar: any;
|
||||
device: String;
|
||||
fixedHeader: Boolean;
|
||||
classes: any;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "layout",
|
||||
components: {
|
||||
Navbar,
|
||||
Sidebar,
|
||||
AppMain,
|
||||
setting,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const WIDTH = ref(992);
|
||||
|
||||
const set: setInter = reactive({
|
||||
sidebar: computed(() => {
|
||||
return store.state.app.sidebar;
|
||||
}),
|
||||
|
||||
device: computed(() => {
|
||||
return store.state.app.device;
|
||||
}),
|
||||
|
||||
fixedHeader: computed(() => {
|
||||
return store.state.settings.fixedHeader;
|
||||
}),
|
||||
|
||||
classes: computed(() => {
|
||||
return {
|
||||
hideSidebar: !set.sidebar.opened,
|
||||
openSidebar: set.sidebar.opened,
|
||||
withoutAnimation: set.sidebar.withoutAnimation,
|
||||
mobile: set.device === "mobile",
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (set.device === "mobile" && !set.sidebar.opened) {
|
||||
store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
}
|
||||
})
|
||||
|
||||
const handleClickOutside = () => {
|
||||
store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
};
|
||||
|
||||
const $_isMobile = () => {
|
||||
const rect = document.body.getBoundingClientRect();
|
||||
return rect.width - 1 < WIDTH.value;
|
||||
};
|
||||
|
||||
const $_resizeHandler = () => {
|
||||
if (!document.hidden) {
|
||||
const isMobile = $_isMobile();
|
||||
store.dispatch("app/toggleDevice", isMobile ? "mobile" : "desktop");
|
||||
|
||||
if (isMobile) {
|
||||
store.dispatch("app/closeSideBar", { withoutAnimation: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const isMobile = $_isMobile();
|
||||
if (isMobile) {
|
||||
store.dispatch("app/toggleDevice", "mobile");
|
||||
store.dispatch("app/closeSideBar", { withoutAnimation: true });
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener("resize", $_resizeHandler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", $_resizeHandler);
|
||||
});
|
||||
|
||||
return {
|
||||
...toRefs(set),
|
||||
handleClickOutside,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
$sideBarWidth: 210px;
|
||||
|
||||
.app-wrapper {
|
||||
@include clearfix;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
14
frontend/vue-ts/src/layout/store.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { computed, ComputedRef } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export function useMapGetters<T extends string>(keys: T[]) {
|
||||
const res: Record<string, ComputedRef> = {}
|
||||
// @ts-ignore
|
||||
const { getters } = useStore()
|
||||
keys.map(key => {
|
||||
if (Reflect.has(getters, key)) {
|
||||
res[key] = computed(() => getters[key])
|
||||
}
|
||||
})
|
||||
return res as any as Record<T, ComputedRef>
|
||||
}
|
11
frontend/vue-ts/src/locales/ch.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"home": "首页",
|
||||
"LoginOut": "退出系统",
|
||||
"usermanagement": "用户管理",
|
||||
"baseinfo": "基础信息",
|
||||
"error": "错误页面",
|
||||
"404": "404",
|
||||
"401": "401",
|
||||
"components": "组件",
|
||||
"split-pane": "切割面板"
|
||||
}
|
11
frontend/vue-ts/src/locales/en.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"home": "Home",
|
||||
"LoginOut": "Login Out",
|
||||
"usermanagement": "User Manage",
|
||||
"baseinfo": "Base Info",
|
||||
"error": "Error Page",
|
||||
"404": "404",
|
||||
"401": "401",
|
||||
"components": "Components",
|
||||
"split-pane": "Split Pane"
|
||||
}
|
35
frontend/vue-ts/src/main.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
// 内置ElementPlus
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/lib/theme-chalk/index.css'
|
||||
|
||||
// 内置vxe-table
|
||||
import 'xe-utils'
|
||||
import VXETable from 'vxe-table'
|
||||
import 'vxe-table/lib/style.css'
|
||||
|
||||
// 内置国际化语言包
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ch from "./locales/ch.json"
|
||||
import en from "./locales/en.json"
|
||||
const i18n = createI18n({
|
||||
locale: 'ch', //默认使用中文
|
||||
messages: {
|
||||
ch,
|
||||
en
|
||||
}
|
||||
})
|
||||
|
||||
// 导入公共样式
|
||||
import './style/index.scss'
|
||||
// 导入字体图标
|
||||
import "./assets/iconfont/iconfont.js"
|
||||
import "./assets/iconfont/iconfont.css"
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(store).use(router).use(i18n).use(ElementPlus).use(VXETable).mount('#app')
|
171
frontend/vue-ts/src/router/index.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"
|
||||
|
||||
import Layout from '../layout/index.vue'
|
||||
|
||||
import { storageSession } from "../utils/storage"
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Layout,
|
||||
redirect: "/welcome",
|
||||
children: [{
|
||||
path: '/welcome',
|
||||
name: 'welcome',
|
||||
component: () => import(/* webpackChunkName: "home" */ '../views/welcome.vue'),
|
||||
meta: {
|
||||
title: 'home',
|
||||
showLink: true,
|
||||
savedPosition: false
|
||||
}
|
||||
}],
|
||||
meta: {
|
||||
icon: 'el-icon-s-home',
|
||||
showLink: true,
|
||||
savedPosition: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/components',
|
||||
name: 'components',
|
||||
component: Layout,
|
||||
redirect: '/components/split-pane',
|
||||
children: [
|
||||
{
|
||||
path: '/components/split-pane',
|
||||
component: () => import(/* webpackChunkName: "components" */ '../views/components/split-pane/index.vue'),
|
||||
meta: {
|
||||
title: 'split-pane',
|
||||
showLink: false,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
icon: 'el-icon-menu',
|
||||
title: 'components',
|
||||
showLink: true,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'user',
|
||||
component: Layout,
|
||||
redirect: '/user/base',
|
||||
children: [
|
||||
{
|
||||
path: '/user/base',
|
||||
component: () => import(/* webpackChunkName: "user" */ '../views/user.vue'),
|
||||
meta: {
|
||||
// icon: 'el-icon-user',
|
||||
title: 'baseinfo',
|
||||
showLink: false,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
icon: 'el-icon-user',
|
||||
title: 'usermanagement',
|
||||
showLink: true,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/error',
|
||||
name: 'error',
|
||||
component: Layout,
|
||||
redirect: '/error/401',
|
||||
children: [
|
||||
{
|
||||
path: '/error/401',
|
||||
component: () => import(/* webpackChunkName: "error" */ '../views/error/401.vue'),
|
||||
meta: {
|
||||
title: '401',
|
||||
showLink: false,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/error/404',
|
||||
component: () => import(/* webpackChunkName: "error" */ '../views/error/404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
showLink: false,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
icon: 'el-icon-position',
|
||||
title: 'error',
|
||||
showLink: true,
|
||||
savedPosition: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
|
||||
meta: {
|
||||
title: '登陆',
|
||||
showLink: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import(/* webpackChunkName: "register" */ '../views/register.vue'),
|
||||
meta: {
|
||||
title: '注册',
|
||||
showLink: false
|
||||
}
|
||||
},
|
||||
{
|
||||
// 找不到路由重定向到404页面
|
||||
path: '/:pathMatch(.*)',
|
||||
component: Layout,
|
||||
redirect: "/error/404",
|
||||
meta: {
|
||||
icon: 'el-icon-s-home',
|
||||
title: '首页',
|
||||
showLink: false,
|
||||
savedPosition: false,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
if (from.meta.saveSrollTop) {
|
||||
const top: number = document.documentElement.scrollTop || document.body.scrollTop
|
||||
resolve({ left: 0, top })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
import NProgress from "../utils/progress"
|
||||
|
||||
const whiteList = ["/login", "/register"]
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
NProgress.start()
|
||||
document.title = to.meta.title // 动态title
|
||||
whiteList.indexOf(to.path) !== -1 || storageSession.getItem("info") ? next() : next("/login") // 全部重定向到登录页
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
export default router
|
9
frontend/vue-ts/src/settings.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
|
||||
title: 'CURE Admin',
|
||||
|
||||
fixedHeader: false,
|
||||
|
||||
sidebarLogo: false
|
||||
|
||||
}
|
10
frontend/vue-ts/src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
const scss: Record<string, string>
|
||||
export default scss;
|
||||
}
|
6
frontend/vue-ts/src/store/getters.ts
Normal file
@ -0,0 +1,6 @@
|
||||
const getters = {
|
||||
sidebar: (state: any) => state.app.sidebar,
|
||||
device: (state: any) => state.app.device,
|
||||
}
|
||||
|
||||
export default getters
|
12
frontend/vue-ts/src/store/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { createStore } from 'vuex'
|
||||
import getters from './getters'
|
||||
import app from './modules/app'
|
||||
import settings from './modules/settings'
|
||||
|
||||
export default createStore({
|
||||
getters,
|
||||
modules: {
|
||||
app,
|
||||
settings
|
||||
}
|
||||
})
|
58
frontend/vue-ts/src/store/modules/app.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { storageLocal } from "../../utils/storage"
|
||||
interface stateInter {
|
||||
sidebar: {
|
||||
opened: Boolean,
|
||||
withoutAnimation: Boolean
|
||||
},
|
||||
device: String
|
||||
}
|
||||
|
||||
const state = {
|
||||
sidebar: {
|
||||
opened: storageLocal.getItem('sidebarStatus') ? !!+storageLocal.getItem('sidebarStatus') : true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
device: 'desktop'
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
TOGGLE_SIDEBAR: (state: stateInter): void => {
|
||||
state.sidebar.opened = !state.sidebar.opened
|
||||
state.sidebar.withoutAnimation = false
|
||||
if (state.sidebar.opened) {
|
||||
storageLocal.setItem('sidebarStatus', 1)
|
||||
} else {
|
||||
storageLocal.setItem('sidebarStatus', 0)
|
||||
}
|
||||
},
|
||||
CLOSE_SIDEBAR: (state: stateInter, withoutAnimation: Boolean) => {
|
||||
storageLocal.setItem('sidebarStatus', 0)
|
||||
state.sidebar.opened = false
|
||||
state.sidebar.withoutAnimation = withoutAnimation
|
||||
},
|
||||
TOGGLE_DEVICE: (state: stateInter, device: String) => {
|
||||
state.device = device
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
// @ts-ignore
|
||||
toggleSideBar({ commit }) {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
},
|
||||
// @ts-ignore
|
||||
closeSideBar({ commit }, { withoutAnimation }) {
|
||||
commit('CLOSE_SIDEBAR', withoutAnimation)
|
||||
},
|
||||
// @ts-ignore
|
||||
toggleDevice({ commit }, device) {
|
||||
commit('TOGGLE_DEVICE', device)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
29
frontend/vue-ts/src/store/modules/settings.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import defaultSettings from '../../settings'
|
||||
|
||||
const state = {
|
||||
title: defaultSettings.title,
|
||||
fixedHeader: defaultSettings.fixedHeader,
|
||||
sidebarLogo: defaultSettings.sidebarLogo
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
CHANGE_SETTING: (state: any, { key, value }) => {
|
||||
if (state.hasOwnProperty(key)) {
|
||||
state[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
changeSetting({ commit }, data) {
|
||||
commit('CHANGE_SETTING', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
|
48
frontend/vue-ts/src/style/element-ui.scss
Normal file
@ -0,0 +1,48 @@
|
||||
// cover some element-plus styles
|
||||
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.el-dialog {
|
||||
transform: none;
|
||||
left: 0;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// refine element ui upload
|
||||
.upload-container {
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropdown
|
||||
.el-dropdown-menu {
|
||||
a {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
|
||||
// to fix el-date-picker css style
|
||||
.el-range-separator {
|
||||
box-sizing: content-box;
|
||||
}
|
102
frontend/vue-ts/src/style/index.scss
Normal file
@ -0,0 +1,102 @@
|
||||
@import './variables.scss';
|
||||
@import './mixin.scss';
|
||||
@import './transition.scss';
|
||||
@import './element-ui.scss';
|
||||
@import './sidebar.scss';
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// main-container global css
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login,
|
||||
.register {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
background: url("../assets/bg.png") no-repeat center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* 头部用户信息样式重置 */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.resetTop {
|
||||
top: 48px !important;
|
||||
}
|
||||
|
||||
.html-grey {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
-moz-filter: grayscale(100%);
|
||||
-ms-filter: grayscale(100%);
|
||||
-o-filter: grayscale(100%);
|
||||
filter: url("data:image/svg+xml;utf8,#grayscale");
|
||||
filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);
|
||||
-webkit-filter: grayscale(1);
|
||||
}
|
28
frontend/vue-ts/src/style/mixin.scss
Normal file
@ -0,0 +1,28 @@
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollBar {
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin relative {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
201
frontend/vue-ts/src/style/sidebar.scss
Normal file
@ -0,0 +1,201 @@
|
||||
#app {
|
||||
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
transition: margin-left .28s;
|
||||
margin-left: $sideBarWidth;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: width 0.28s;
|
||||
width: $sideBarWidth !important;
|
||||
background-color: $menuBg;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
font-size: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
|
||||
// reset element-plus css
|
||||
.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__bar.is-vertical {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
// width: 100% !important;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.submenu-title-noDropdown,
|
||||
.el-submenu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active>.el-submenu__title {
|
||||
color: $subMenuActiveText !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .el-submenu .el-menu-item {
|
||||
min-width: $sideBarWidth !important;
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
.submenu-title-noDropdown {
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
|
||||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
overflow: hidden;
|
||||
&>.el-submenu__title {
|
||||
|
||||
.el-submenu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
margin-left: -5px; //需优化的地方
|
||||
.el-submenu {
|
||||
&>.el-submenu__title {
|
||||
&>span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse .el-menu .el-submenu {
|
||||
min-width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
// mobile responsive
|
||||
.mobile {
|
||||
.main-container {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: transform .28s;
|
||||
width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
&.hideSidebar {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withoutAnimation {
|
||||
|
||||
.main-container,
|
||||
.sidebar-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when menu collapsed
|
||||
.el-menu--vertical {
|
||||
&>.el-menu {
|
||||
i {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu .el-submenu>.el-submenu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
// the scroll bar appears when the subMenu is too long
|
||||
>.el-menu--popup {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap { overflow: auto; height: 100%; }
|
44
frontend/vue-ts/src/style/transition.scss
Normal file
@ -0,0 +1,44 @@
|
||||
// global transition css
|
||||
|
||||
/* 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 .5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* breadcrumb transition */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
}
|
22
frontend/vue-ts/src/style/variables.scss
Normal file
@ -0,0 +1,22 @@
|
||||
// sidebar
|
||||
$menuText:#bfcbd9;
|
||||
$menuActiveText:#409EFF;
|
||||
$subMenuActiveText:#f4f4f5;
|
||||
$menuBg:#304156;
|
||||
$menuHover:#263445;
|
||||
|
||||
$subMenuBg:#1f2d3d;
|
||||
$subMenuHover:#001528;
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
|
||||
:export {
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
subMenuActiveText: $subMenuActiveText;
|
||||
menuBg: $menuBg;
|
||||
menuHover: $menuHover;
|
||||
subMenuBg: $subMenuBg;
|
||||
subMenuHover: $subMenuHover;
|
||||
sideBarWidth: $sideBarWidth;
|
||||
}
|
24
frontend/vue-ts/src/utils/algorithm/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
interface ProxyAlgorithm {
|
||||
increaseIndexes<T>(val: Array<T>): Array<T>
|
||||
}
|
||||
|
||||
class algorithmProxy implements ProxyAlgorithm {
|
||||
|
||||
constructor() { }
|
||||
|
||||
// 数组每一项添加索引字段
|
||||
public increaseIndexes<T>(val: Array<T>): Array<T> {
|
||||
return Object.keys(val)
|
||||
.map((v) => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
...val[v],
|
||||
key: v
|
||||
}
|
||||
})
|
||||
.filter(v => v.meta.showLink)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const algorithm = new algorithmProxy()
|
11
frontend/vue-ts/src/utils/debounce/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// 延迟函数
|
||||
export const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout))
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = (fn: () => any, timeout: number) => {
|
||||
let timmer: any
|
||||
return () => {
|
||||
timmer ? clearTimeout(timmer) : null
|
||||
timmer = setTimeout(fn, timeout)
|
||||
}
|
||||
}
|
31
frontend/vue-ts/src/utils/http/config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { AxiosRequestConfig } from "axios"
|
||||
import { excludeProps } from "./utils"
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
export const defaultConfig: AxiosRequestConfig = {
|
||||
baseURL: '/api',
|
||||
timeout: 10000, //10秒超时
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
}
|
||||
|
||||
export function genConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
|
||||
if (!config) {
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
const { headers } = config
|
||||
if (headers && typeof headers === "object") {
|
||||
defaultConfig.headers = {
|
||||
...defaultConfig.headers,
|
||||
...headers
|
||||
}
|
||||
}
|
||||
return { ...excludeProps(config!, "headers"), ...defaultConfig }
|
||||
}
|
||||
|
||||
export const METHODS = ["post", "get", "put", "delete", "option", "patch"]
|
244
frontend/vue-ts/src/utils/http/core.ts
Normal file
@ -0,0 +1,244 @@
|
||||
|
||||
import Axios, {
|
||||
AxiosRequestConfig,
|
||||
CancelTokenStatic,
|
||||
AxiosInstance,
|
||||
Canceler
|
||||
} from "axios"
|
||||
|
||||
import NProgress from "../progress"
|
||||
|
||||
import { genConfig } from "./config"
|
||||
|
||||
import { transformConfigByMethod } from "./utils"
|
||||
|
||||
import {
|
||||
cancelTokenType,
|
||||
RequestMethods,
|
||||
EnclosureHttpRequestConfig,
|
||||
EnclosureHttpResoponse,
|
||||
EnclosureHttpError
|
||||
} from "./types.d"
|
||||
|
||||
class EnclosureHttp {
|
||||
constructor() {
|
||||
this.httpInterceptorsRequest()
|
||||
this.httpInterceptorsResponse()
|
||||
}
|
||||
// 初始化配置对象
|
||||
private static initConfig: EnclosureHttpRequestConfig = {};
|
||||
|
||||
// 保存当前Axios实例对象
|
||||
private static axiosInstance: AxiosInstance = Axios.create(genConfig());
|
||||
|
||||
// 保存 EnclosureHttp实例
|
||||
private static EnclosureHttpInstance: EnclosureHttp
|
||||
|
||||
// axios取消对象
|
||||
private CancelToken: CancelTokenStatic = Axios.CancelToken;
|
||||
|
||||
// 取消的凭证数组
|
||||
private sourceTokenList: Array<cancelTokenType> = [];
|
||||
|
||||
// 记录当前这一次cancelToken的key
|
||||
private currentCancelTokenKey = "";
|
||||
|
||||
private beforeRequestCallback: EnclosureHttpRequestConfig["beforeRequestCallback"] = undefined;
|
||||
|
||||
private beforeResponseCallback: EnclosureHttpRequestConfig["beforeResponseCallback"] = undefined;
|
||||
|
||||
public get cancelTokenList(): Array<cancelTokenType> {
|
||||
return this.sourceTokenList
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public set cancelTokenList(value) {
|
||||
throw new Error("cancelTokenList不允许赋值")
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 私有构造不允许实例化
|
||||
* @returns void 0
|
||||
*/
|
||||
// constructor() {}
|
||||
|
||||
/**
|
||||
* @description 生成唯一取消key
|
||||
* @param config axios配置
|
||||
* @returns string
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private genUniqueKey(config: EnclosureHttpRequestConfig): string {
|
||||
return `${config.url}--${JSON.stringify(config.data)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 取消重复请求
|
||||
* @returns void 0
|
||||
*/
|
||||
private cancelRepeatRequest(): void {
|
||||
const temp: { [key: string]: boolean } = {}
|
||||
|
||||
this.sourceTokenList = this.sourceTokenList.reduce<Array<cancelTokenType>>(
|
||||
(res: Array<cancelTokenType>, cancelToken: cancelTokenType) => {
|
||||
const { cancelKey, cancelExecutor } = cancelToken
|
||||
if (!temp[cancelKey]) {
|
||||
temp[cancelKey] = true
|
||||
res.push(cancelToken)
|
||||
} else {
|
||||
cancelExecutor()
|
||||
}
|
||||
return res
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 删除指定的CancelToken
|
||||
* @returns void 0
|
||||
*/
|
||||
private deleteCancelTokenByCancelKey(cancelKey: string): void {
|
||||
this.sourceTokenList =
|
||||
this.sourceTokenList.length < 1
|
||||
? this.sourceTokenList.filter(
|
||||
cancelToken => cancelToken.cancelKey !== cancelKey
|
||||
)
|
||||
: []
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 拦截请求
|
||||
* @returns void 0
|
||||
*/
|
||||
|
||||
private httpInterceptorsRequest(): void {
|
||||
EnclosureHttp.axiosInstance.interceptors.request.use(
|
||||
(config: EnclosureHttpRequestConfig) => {
|
||||
const $config = config
|
||||
NProgress.start() // 每次切换页面时,调用进度条
|
||||
const cancelKey = this.genUniqueKey($config)
|
||||
$config.cancelToken = new this.CancelToken((cancelExecutor: (cancel: any) => void) => {
|
||||
this.sourceTokenList.push({ cancelKey, cancelExecutor })
|
||||
})
|
||||
this.cancelRepeatRequest()
|
||||
this.currentCancelTokenKey = cancelKey
|
||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||
if (typeof this.beforeRequestCallback === "function") {
|
||||
this.beforeRequestCallback($config)
|
||||
this.beforeRequestCallback = undefined
|
||||
return $config
|
||||
}
|
||||
if (EnclosureHttp.initConfig.beforeRequestCallback) {
|
||||
EnclosureHttp.initConfig.beforeRequestCallback($config)
|
||||
return $config
|
||||
}
|
||||
return $config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清空当前cancelTokenList
|
||||
* @returns void 0
|
||||
*/
|
||||
public clearCancelTokenList(): void {
|
||||
this.sourceTokenList.length = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 拦截相应
|
||||
* @returns void 0
|
||||
*/
|
||||
private httpInterceptorsResponse(): void {
|
||||
const instance = EnclosureHttp.axiosInstance
|
||||
instance.interceptors.response.use(
|
||||
(response: EnclosureHttpResoponse) => {
|
||||
// 请求每次成功一次就删除当前canceltoken标记
|
||||
const cancelKey = this.genUniqueKey(response.config)
|
||||
this.deleteCancelTokenByCancelKey(cancelKey)
|
||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||
if (typeof this.beforeResponseCallback === "function") {
|
||||
this.beforeResponseCallback(response)
|
||||
this.beforeResponseCallback = undefined
|
||||
return response.data
|
||||
}
|
||||
if (EnclosureHttp.initConfig.beforeResponseCallback) {
|
||||
EnclosureHttp.initConfig.beforeResponseCallback(response)
|
||||
return response.data
|
||||
}
|
||||
NProgress.done()
|
||||
return response.data
|
||||
},
|
||||
(error: EnclosureHttpError) => {
|
||||
const $error = error
|
||||
// 判断当前的请求中是否在 取消token数组理存在,如果存在则移除(单次请求流程)
|
||||
if (this.currentCancelTokenKey) {
|
||||
const haskey = this.sourceTokenList.filter(
|
||||
cancelToken => cancelToken.cancelKey === this.currentCancelTokenKey
|
||||
).length
|
||||
if (haskey) {
|
||||
this.sourceTokenList = this.sourceTokenList.filter(
|
||||
cancelToken =>
|
||||
cancelToken.cancelKey !== this.currentCancelTokenKey
|
||||
)
|
||||
this.currentCancelTokenKey = ""
|
||||
}
|
||||
}
|
||||
$error.isCancelRequest = Axios.isCancel($error)
|
||||
// 所有的响应异常 区分来源为取消请求/非取消请求
|
||||
return Promise.reject($error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public request<T>(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
param?: AxiosRequestConfig,
|
||||
axiosConfig?: EnclosureHttpRequestConfig,
|
||||
): Promise<T> {
|
||||
const config = transformConfigByMethod(param, {
|
||||
method,
|
||||
url,
|
||||
...axiosConfig
|
||||
} as EnclosureHttpRequestConfig)
|
||||
// 单独处理自定义请求/响应回掉
|
||||
if (axiosConfig?.beforeRequestCallback) {
|
||||
this.beforeRequestCallback = axiosConfig.beforeRequestCallback
|
||||
}
|
||||
if (axiosConfig?.beforeResponseCallback) {
|
||||
this.beforeResponseCallback = axiosConfig.beforeResponseCallback
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
EnclosureHttp.axiosInstance.request(config)
|
||||
.then((response: EnclosureHttpResoponse) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public post<T>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: EnclosureHttpRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request<T>("post", url, params, config)
|
||||
}
|
||||
|
||||
public get<T>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: EnclosureHttpRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request<T>("get", url, params, config)
|
||||
}
|
||||
}
|
||||
|
||||
export default EnclosureHttp
|
3
frontend/vue-ts/src/utils/http/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import EnclosureHttp from "./core"
|
||||
export const http = new EnclosureHttp()
|
||||
|
48
frontend/vue-ts/src/utils/http/types.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
import Axios, {
|
||||
AxiosRequestConfig,
|
||||
Canceler,
|
||||
AxiosResponse,
|
||||
Method,
|
||||
AxiosError
|
||||
} from "axios"
|
||||
|
||||
import { METHODS } from './config'
|
||||
|
||||
export type cancelTokenType = { cancelKey: string, cancelExecutor: Canceler }
|
||||
|
||||
export type RequestMethods = Extract<Method, 'get' | 'post' | 'put' | 'delete' | 'patch' |
|
||||
'option' | 'head'>
|
||||
|
||||
export interface EnclosureHttpRequestConfig extends AxiosRequestConfig {
|
||||
beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void // 请求发送之前
|
||||
beforeResponseCallback?: (response: EnclosureHttpResoponse) => void // 相应返回之前
|
||||
}
|
||||
|
||||
export interface EnclosureHttpResoponse extends AxiosResponse {
|
||||
config: EnclosureHttpRequestConfig
|
||||
}
|
||||
|
||||
export interface EnclosureHttpError extends AxiosError {
|
||||
isCancelRequest?: boolean
|
||||
}
|
||||
|
||||
export default class EnclosureHttp {
|
||||
cancelTokenList: Array<cancelTokenType>
|
||||
clearCancelTokenList(): void
|
||||
request<T>(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
param?: AxiosRequestConfig,
|
||||
axiosConfig?: EnclosureHttpRequestConfig
|
||||
): Promise<T>
|
||||
post<T>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: EnclosureHttpRequestConfig
|
||||
): Promise<T>
|
||||
get<T>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: EnclosureHttpRequestConfig
|
||||
): Promise<T>
|
||||
}
|
29
frontend/vue-ts/src/utils/http/utils.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { EnclosureHttpRequestConfig } from "./types.d"
|
||||
|
||||
export function excludeProps<T extends { [key: string]: any }>(
|
||||
origin: T,
|
||||
prop: string
|
||||
): { [key: string]: T } {
|
||||
return Object.keys(origin)
|
||||
.filter(key => !prop.includes(key))
|
||||
.reduce((res, key) => {
|
||||
res[key] = origin[key]
|
||||
return res
|
||||
}, {} as { [key: string]: T })
|
||||
}
|
||||
|
||||
export function transformConfigByMethod(
|
||||
params: any,
|
||||
config: EnclosureHttpRequestConfig
|
||||
): EnclosureHttpRequestConfig {
|
||||
const { method } = config
|
||||
const props = ["delete", "get", "head", "options"].includes(
|
||||
method!.toLocaleLowerCase()
|
||||
)
|
||||
? "params"
|
||||
: "data"
|
||||
return {
|
||||
...config,
|
||||
[props]: params
|
||||
}
|
||||
}
|
54
frontend/vue-ts/src/utils/loaders/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
interface ProxyLoader {
|
||||
loadCss(src: string): any
|
||||
loadScript(src: string): Promise<any>
|
||||
loadScriptConcurrent(src: Array<string>): Promise<any>
|
||||
}
|
||||
|
||||
class loaderProxy implements ProxyLoader {
|
||||
|
||||
constructor() { }
|
||||
|
||||
protected scriptLoaderCache: Array<string> = []
|
||||
|
||||
public loadCss = (src: string): any => {
|
||||
let element = document.createElement("link")
|
||||
element.rel = "stylesheet"
|
||||
element.href = src
|
||||
document.body.appendChild(element)
|
||||
}
|
||||
|
||||
public loadScript = async (src: string): Promise<any> => {
|
||||
if (this.scriptLoaderCache.includes(src)) {
|
||||
return src
|
||||
} else {
|
||||
let element: Element = document.createElement("script")
|
||||
element.src = src
|
||||
document.body.appendChild(element)
|
||||
element.onload = () => {
|
||||
return this.scriptLoaderCache.push(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public loadScriptConcurrent = async (srcList: Array<string>): Promise<any> => {
|
||||
if (Array.isArray(srcList)) {
|
||||
const len: number = srcList.length
|
||||
if (len > 0) {
|
||||
let count: number = 0
|
||||
srcList.map(src => {
|
||||
if (src) {
|
||||
this.loadScript(src).then(() => {
|
||||
count++
|
||||
if (count === len) {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const loader = new loaderProxy()
|
43
frontend/vue-ts/src/utils/message/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ElMessage } from "element-plus"
|
||||
|
||||
// 消息
|
||||
const Message = (message: string): any => {
|
||||
return ElMessage({
|
||||
showClose: true,
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
// 成功
|
||||
const successMessage = (message: string): any => {
|
||||
return ElMessage({
|
||||
showClose: true,
|
||||
message,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
|
||||
// 警告
|
||||
const warnMessage = (message: string): any => {
|
||||
return ElMessage({
|
||||
showClose: true,
|
||||
message,
|
||||
type: "warning"
|
||||
})
|
||||
}
|
||||
|
||||
// 失败
|
||||
const errorMessage = (message: string): any => {
|
||||
return ElMessage({
|
||||
showClose: true,
|
||||
message,
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
Message,
|
||||
successMessage,
|
||||
warnMessage,
|
||||
errorMessage
|
||||
}
|
14
frontend/vue-ts/src/utils/operate/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const hasClass = (ele: Element, cls:string) :any => {
|
||||
return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
|
||||
}
|
||||
|
||||
export const addClass = (ele: Element, cls:string) :any => {
|
||||
if (!hasClass(ele, cls)) ele.className += ' ' + cls
|
||||
}
|
||||
|
||||
export const removeClass =(ele: Element, cls:string) :any => {
|
||||
if (hasClass(ele, cls)) {
|
||||
const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
|
||||
ele.className = ele.className.replace(reg, ' ')
|
||||
}
|
||||
}
|
12
frontend/vue-ts/src/utils/progress/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import NProgress from "nprogress"
|
||||
import "nprogress/nprogress.css"
|
||||
|
||||
NProgress.configure({
|
||||
easing: 'ease', // 动画方式
|
||||
speed: 500, // 递增进度条的速度
|
||||
showSpinner: true, // 是否显示加载ico
|
||||
trickleSpeed: 200, // 自动递增间隔
|
||||
minimum: 0.3 // 初始化时的最小百分比
|
||||
})
|
||||
|
||||
export default NProgress
|
32
frontend/vue-ts/src/utils/resize/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
|
||||
const resizeHandler = (entries: any[]): void => {
|
||||
for (const entry of entries) {
|
||||
const listeners = entry.target.__resizeListeners__ || []
|
||||
if (listeners.length) {
|
||||
listeners.forEach((fn: () => any) => {
|
||||
fn()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const addResizeListener = (element: any, fn: () => any): any => {
|
||||
if (isServer) return
|
||||
if (!element.__resizeListeners__) {
|
||||
element.__resizeListeners__ = []
|
||||
element.__ro__ = new ResizeObserver(resizeHandler)
|
||||
element.__ro__.observe(element)
|
||||
}
|
||||
element.__resizeListeners__.push(fn)
|
||||
}
|
||||
|
||||
export const removeResizeListener = (element: any, fn: () => any): any => {
|
||||
if (!element || !element.__resizeListeners__) return
|
||||
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
|
||||
if (!element.__resizeListeners__.length) {
|
||||
element.__ro__.disconnect()
|
||||
}
|
||||
}
|
42
frontend/vue-ts/src/utils/storage/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
interface ProxyStorage {
|
||||
getItem(key: string): any
|
||||
setItem(Key: string, value: string): void
|
||||
removeItem(key: string): void
|
||||
}
|
||||
|
||||
//sessionStorage operate
|
||||
class sessionStorageProxy implements ProxyStorage {
|
||||
|
||||
protected storage: ProxyStorage
|
||||
|
||||
constructor(storageModel: ProxyStorage) {
|
||||
this.storage = storageModel
|
||||
}
|
||||
|
||||
// 存
|
||||
public setItem(key: string, value: any): void {
|
||||
this.storage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
// 取
|
||||
public getItem(key: string): any {
|
||||
return JSON.parse(this.storage.getItem(key)) || null
|
||||
}
|
||||
|
||||
// 删
|
||||
public removeItem(key: string): void {
|
||||
this.storage.removeItem(key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//localStorage operate
|
||||
class localStorageProxy extends sessionStorageProxy implements ProxyStorage {
|
||||
constructor(localStorage: ProxyStorage) {
|
||||
super(localStorage)
|
||||
}
|
||||
}
|
||||
|
||||
export const storageSession = new sessionStorageProxy(sessionStorage)
|
||||
|
||||
export const storageLocal = new localStorageProxy(localStorage)
|
85
frontend/vue-ts/src/views/components/split-pane/index.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="split-pane">
|
||||
<splitpane :splitSet="settingLR">
|
||||
<!-- #paneL 表示指定该组件为左侧面板 -->
|
||||
<template #paneL>
|
||||
<!-- 自定义左侧面板的内容 -->
|
||||
<div class="dv-a">A</div>
|
||||
</template>
|
||||
<!-- #paneR 表示指定该组件为右侧面板 -->
|
||||
<template #paneR>
|
||||
<!-- 再次将右侧面板进行拆分 -->
|
||||
<splitpane :splitSet="settingTB">
|
||||
<template #paneL>
|
||||
<div class="dv-b">B</div>
|
||||
</template>
|
||||
<template #paneR>
|
||||
<div class="dv-c">C</div>
|
||||
</template>
|
||||
</splitpane>
|
||||
</template>
|
||||
</splitpane>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import splitpane, {
|
||||
ContextProps,
|
||||
} from "../../../components/splitPane/index.vue";
|
||||
import { reactive } from "vue";
|
||||
export default {
|
||||
name: "split",
|
||||
components: {
|
||||
splitpane,
|
||||
},
|
||||
setup() {
|
||||
const settingLR: ContextProps = reactive({
|
||||
minPercent: 20,
|
||||
defaultPercent: 40,
|
||||
split: "vertical",
|
||||
});
|
||||
|
||||
const settingTB: ContextProps = reactive({
|
||||
minPercent: 20,
|
||||
defaultPercent: 40,
|
||||
split: "horizontal",
|
||||
});
|
||||
|
||||
return {
|
||||
settingLR,
|
||||
settingTB,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$W: 100%;
|
||||
$H: 80vh;
|
||||
.split-pane {
|
||||
width: 98%;
|
||||
height: $H;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 50px;
|
||||
color: #fff;
|
||||
.dv-a,
|
||||
.dv-b,
|
||||
.dv-c {
|
||||
width: $W;
|
||||
height: $W;
|
||||
background: rgba($color: dodgerblue, $alpha: 0.8);
|
||||
line-height: $H;
|
||||
}
|
||||
.dv-b,
|
||||
.dv-c {
|
||||
line-height: 250px;
|
||||
}
|
||||
.dv-b {
|
||||
background: rgba($color: #000, $alpha: 0.8);
|
||||
}
|
||||
.dv-c {
|
||||
background: rgba($color: #ce272d, $alpha: 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
73
frontend/vue-ts/src/views/error/401.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">CURD Admin</h1>
|
||||
<h2>你没有权限去该页面</h2>
|
||||
<h6>如有不满请联系你领导</h6>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<img
|
||||
:src="img"
|
||||
width="313"
|
||||
height="428"
|
||||
alt="Girl has dropped her ice cream."
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import imgs from '/@/assets/401.gif'
|
||||
import { ref } from "vue"
|
||||
export default {
|
||||
name: "401",
|
||||
setup() {
|
||||
const img = ref(`${imgs}?${new Date()}`)
|
||||
return {
|
||||
img
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.errPage-container {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
.pan-back-btn {
|
||||
background: #008489;
|
||||
color: #fff;
|
||||
border: none !important;
|
||||
}
|
||||
.pan-gif {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
.pan-img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.text-jumbo {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
color: #484848;
|
||||
}
|
||||
.list-unstyled {
|
||||
font-size: 14px;
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
a {
|
||||
color: #008489;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
236
frontend/vue-ts/src/views/error/404.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" :src="four" alt="404" />
|
||||
<img class="pic-404__child left" :src="four_cloud" alt="404" />
|
||||
<img class="pic-404__child mid" :src="four_cloud" alt="404" />
|
||||
<img class="pic-404__child right" :src="four_cloud" alt="404" />
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">CURD Admin</div>
|
||||
<div class="bullshit__headline">{{ message }}</div>
|
||||
<div class="bullshit__info">
|
||||
Please check that the URL you entered is correct, or click the button
|
||||
below to return to the homepage.
|
||||
</div>
|
||||
<a href="" class="bullshit__return-home">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from "vue";
|
||||
import four from "/@/assets/404.png";
|
||||
import four_cloud from "/@/assets/404_cloud.png";
|
||||
export default {
|
||||
name: "404",
|
||||
setup() {
|
||||
const message = computed(() => {
|
||||
return "The webmaster said that you can not enter this page...";
|
||||
});
|
||||
|
||||
return {
|
||||
message,
|
||||
four,
|
||||
four_cloud,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wscn-http404-container {
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
84
frontend/vue-ts/src/views/login.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<info
|
||||
:ruleForm="contextInfo"
|
||||
@on-behavior="onLogin"
|
||||
@refreshVerify="refreshVerify"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
onBeforeMount,
|
||||
getCurrentInstance,
|
||||
} from "vue";
|
||||
import info, { ContextProps } from "../components/info/index.vue";
|
||||
import { getVerify, getLogin } from "../api/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import { storageSession } from "../utils/storage";
|
||||
import { warnMessage, successMessage } from "../utils/message";
|
||||
export default {
|
||||
name: "login",
|
||||
components: {
|
||||
info,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
|
||||
// 刷新验证码
|
||||
const refreshGetVerify = async () => {
|
||||
let { svg } = await getVerify();
|
||||
contextInfo.svg = svg;
|
||||
};
|
||||
|
||||
const contextInfo: ContextProps = reactive({
|
||||
userName: "",
|
||||
passWord: "",
|
||||
verify: null,
|
||||
svg: null,
|
||||
});
|
||||
|
||||
const toPage = (info: Object): void => {
|
||||
storageSession.setItem("info", info);
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// 登录
|
||||
const onLogin = async () => {
|
||||
let { userName, passWord, verify } = contextInfo;
|
||||
let { code, info, accessToken } = await getLogin({
|
||||
username: userName,
|
||||
password: passWord,
|
||||
verify: verify,
|
||||
});
|
||||
code === 0
|
||||
? successMessage(info) &&
|
||||
toPage({
|
||||
username: userName,
|
||||
accessToken,
|
||||
})
|
||||
: warnMessage(info);
|
||||
};
|
||||
|
||||
const refreshVerify = (): void => {
|
||||
refreshGetVerify();
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
refreshGetVerify();
|
||||
});
|
||||
|
||||
return {
|
||||
contextInfo,
|
||||
onLogin,
|
||||
router,
|
||||
toPage,
|
||||
refreshVerify,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
73
frontend/vue-ts/src/views/register.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="register">
|
||||
<info
|
||||
:ruleForm="contextInfo"
|
||||
@on-behavior="onRegist"
|
||||
@refreshVerify="refreshVerify"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
onBeforeMount,
|
||||
getCurrentInstance,
|
||||
} from "vue";
|
||||
import info, { ContextProps } from "../components/info/index.vue";
|
||||
import { getRegist, getVerify } from "../api/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import { warnMessage, successMessage } from "../utils/message";
|
||||
export default {
|
||||
name: "register",
|
||||
components: {
|
||||
info,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
|
||||
// 刷新验证码
|
||||
const refreshGetVerify = async () => {
|
||||
let { svg } = await getVerify();
|
||||
contextInfo.svg = svg;
|
||||
};
|
||||
|
||||
const contextInfo: ContextProps = reactive({
|
||||
userName: "",
|
||||
passWord: "",
|
||||
verify: null,
|
||||
svg: null,
|
||||
});
|
||||
|
||||
// 注册
|
||||
const onRegist = async () => {
|
||||
let { userName, passWord, verify } = contextInfo;
|
||||
let { code, info } = await getRegist({
|
||||
username: userName,
|
||||
password: passWord,
|
||||
verify: verify,
|
||||
});
|
||||
code === 0
|
||||
? successMessage(info) && router.push("/login")
|
||||
: warnMessage(info);
|
||||
};
|
||||
|
||||
const refreshVerify = (): void => {
|
||||
refreshGetVerify();
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
refreshGetVerify();
|
||||
});
|
||||
|
||||
return {
|
||||
contextInfo,
|
||||
onRegist,
|
||||
router,
|
||||
refreshVerify,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
15
frontend/vue-ts/src/views/user.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>用户管理页面</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
export default {
|
||||
name: "user",
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
26
frontend/vue-ts/src/views/welcome.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<a
|
||||
title="欢迎Star"
|
||||
href="https://github.com/xiaoxian521/CURD-TS/tree/vue-ts"
|
||||
target="_blank"
|
||||
>点击打开仓库地址</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
export default {
|
||||
name: "welcome",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("../assets/welcome.png") no-repeat center;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
45
frontend/vue-ts/tsconfig.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true, // 包含导入的模块。json的扩展
|
||||
"lib": [
|
||||
"dom",
|
||||
"esnext"
|
||||
],
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"/@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"types": ["node"],
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"src/utils/path.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.js"
|
||||
],
|
||||
}
|
45
frontend/vue-ts/vite.config.ts
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
import { resolve } from 'path'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import type { UserConfig } from 'vite'
|
||||
import { loadEnv } from './build/utils'
|
||||
import { createProxy } from './build/proxy'
|
||||
|
||||
const pathResolve = (dir: string): any => {
|
||||
return resolve(__dirname, '.', dir)
|
||||
}
|
||||
|
||||
const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_OPEN } = loadEnv()
|
||||
|
||||
const alias: Record<string, string> = {
|
||||
'/@': pathResolve('src'),
|
||||
}
|
||||
|
||||
const root: string = process.cwd()
|
||||
|
||||
const viteConfig: UserConfig = {
|
||||
/**
|
||||
* 基本公共路径
|
||||
* @default '/'
|
||||
*/
|
||||
base: process.env.NODE_ENV === "production" ? "./" : VITE_PUBLIC_PATH,
|
||||
root,
|
||||
alias,
|
||||
// 服务端渲染
|
||||
server: {
|
||||
// 是否开启 https
|
||||
https: false,
|
||||
/**
|
||||
* 端口号
|
||||
* @default 3000
|
||||
*/
|
||||
port: VITE_PORT,
|
||||
// 本地跨域代理
|
||||
proxy: createProxy(VITE_PROXY)
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
}
|
||||
|
||||
export default viteConfig
|