拉取vue版本代码

This commit is contained in:
zhangyiming 2021-01-30 15:03:55 +08:00
parent 58b58e687f
commit 04271f3ea6
90 changed files with 5346 additions and 2 deletions

View File

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

View 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" ] ]

View File

@ -0,0 +1,2 @@
# public path
VITE_PUBLIC_PATH = /manages/

4
frontend/vue-ts/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
/dist
.DS_Store
src/.DS_Store

View File

@ -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 = {}
issueshttps://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
issueshttps://github.com/vuejs/vue-next/issues/2027

View File

@ -0,0 +1,6 @@
const productPlugins = []
process.env.NODE_ENV === "production" && productPlugins.push("transform-remove-console")
module.exports = {
plugins: [...productPlugins],
}

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

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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [require('autoprefixer'), require('postcss-import')],
};

11
frontend/vue-ts/public/animate.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

View File

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View 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";
}

Binary file not shown.

View 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);

View 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
}
]
}

View 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="&#58922;" 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="&#58923;" 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,11 @@
{
"home": "首页",
"LoginOut": "退出系统",
"usermanagement": "用户管理",
"baseinfo": "基础信息",
"error": "错误页面",
"404": "404",
"401": "401",
"components": "组件",
"split-pane": "切割面板"
}

View 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"
}

View 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')

View 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

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

View File

@ -0,0 +1,6 @@
const getters = {
sidebar: (state: any) => state.app.sidebar,
device: (state: any) => state.app.device,
}
export default getters

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

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

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

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

View 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);
}

View 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%;
}

View 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%; }

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

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

View 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()

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

View 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"]

View 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

View File

@ -0,0 +1,3 @@
import EnclosureHttp from "./core"
export const http = new EnclosureHttp()

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

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

View 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()

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

View 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, ' ')
}
}

View 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

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

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

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

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

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

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

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

View File

@ -0,0 +1,15 @@
<template>
<div>用户管理页面</div>
</template>
<script lang='ts'>
export default {
name: "user",
setup() {
return {};
},
};
</script>
<style scoped>
</style>

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

View 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"
],
}

View 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