初始化项目文件
3
web_vue/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
0
web_vue/README.md
Normal file
13
web_vue/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ylsa</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
2661
web_vue/package-lock.json
generated
Normal file
36
web_vue/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "web_vue",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@types/node": "^20.14.10",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "1.8.2",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.9.0",
|
||||
"hls.js": "^1.5.20",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"md-editor-v3": "^4.21.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-request": "^2.0.4",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-video-player": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/luxon": "^3.4.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "6.2.3",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
}
|
6
web_vue/public/docker_icon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Docker" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#fff"/><path stroke="#066da5" stroke-width="38" d="M296 226h42m-92 0h42m-91 0h42m-91 0h41m-91 0h42m8-46h41m8 0h42m7 0h42m-42-46h42"/><path fill="#066da5" d="m472 228s-18-17-55-11c-4-29-35-46-35-46s-29 35-8 74c-6 3-16 7-31 7H68c-5 19-5 145 133 145 99 0 173-46 208-130 52 4 63-39 63-39"/></svg>
|
After Width: | Height: | Size: 431 B |
BIN
web_vue/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
web_vue/public/nginx_icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>file_type_nginx</title><path d="M15.948,2h.065a10.418,10.418,0,0,1,.972.528Q22.414,5.65,27.843,8.774a.792.792,0,0,1,.414.788c-.008,4.389,0,8.777-.005,13.164a.813.813,0,0,1-.356.507q-5.773,3.324-11.547,6.644a.587.587,0,0,1-.657.037Q9.912,26.6,4.143,23.274a.7.7,0,0,1-.4-.666q0-6.582,0-13.163a.693.693,0,0,1,.387-.67Q9.552,5.657,14.974,2.535c.322-.184.638-.379.974-.535" style="fill:#019639"/><path d="M8.767,10.538q0,5.429,0,10.859a1.509,1.509,0,0,0,.427,1.087,1.647,1.647,0,0,0,2.06.206,1.564,1.564,0,0,0,.685-1.293c0-2.62-.005-5.24,0-7.86q3.583,4.29,7.181,8.568a2.833,2.833,0,0,0,2.6.782,1.561,1.561,0,0,0,1.251-1.371q.008-5.541,0-11.081a1.582,1.582,0,0,0-3.152,0c0,2.662-.016,5.321,0,7.982-2.346-2.766-4.663-5.556-7-8.332A2.817,2.817,0,0,0,10.17,9.033,1.579,1.579,0,0,0,8.767,10.538Z" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 879 B |
1
web_vue/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
69
web_vue/src/App.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import YlLayout from "@/components/yl-layout/layout.vue";
|
||||
import YlLoginPage from "@/pages/login/index.vue";
|
||||
import { message, userinfo } from "@/store/store.ts";
|
||||
import { getCookie } from "@/utils/cookie.ts";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { watch } from "vue";
|
||||
import _ from "lodash";
|
||||
import { updateMenu } from "@/utils/menu.ts";
|
||||
|
||||
// 初始化websocket
|
||||
let ws;
|
||||
if (!ws) {
|
||||
ws = import.meta.env.PROD
|
||||
? new WebSocket(`wss://${location.hostname}/ws`)
|
||||
: new WebSocket(`ws://localhost:8080/ws`);
|
||||
}
|
||||
ws.onopen = () => console.log("websocket已连接");
|
||||
ws.onmessage = (e) => (message.online = Number(e.data));
|
||||
// 用户自动登录
|
||||
const { runAsync: autoLogin } = useRequest(() => myRequest(`/api/user/auto`), {
|
||||
manual: true,
|
||||
});
|
||||
// 获取用户信息
|
||||
const { data: getUserinfo } = useRequest(
|
||||
() => myRequest(`/api/user`, userinfo.token, {}, "get"),
|
||||
{ refreshDeps: [() => userinfo.token], ready: () => Boolean(userinfo.token) },
|
||||
);
|
||||
// 初始化菜单信息
|
||||
updateMenu();
|
||||
// 刷新页面保存登录状态
|
||||
if (getCookie("is_login") && getCookie("token")) {
|
||||
// cookie存在登录信息
|
||||
userinfo.login = true;
|
||||
userinfo.token = getCookie("token");
|
||||
} else if (getCookie("deviceId")) {
|
||||
// cookie不存在登录信息,设置了自动登录
|
||||
userinfo.login = true;
|
||||
autoLogin()
|
||||
.then(() => {
|
||||
userinfo.login = true;
|
||||
userinfo.token = getCookie("token");
|
||||
})
|
||||
.catch(() => {
|
||||
userinfo.login = false;
|
||||
userinfo.token = "";
|
||||
});
|
||||
} else {
|
||||
userinfo.login = false;
|
||||
userinfo.token = "";
|
||||
}
|
||||
|
||||
// 获取用户信息后更新状态
|
||||
watch(getUserinfo, () => {
|
||||
userinfo.username = _.get(getUserinfo, "value.data.username", "");
|
||||
userinfo.userinfo = _.get(getUserinfo, "value.data", {});
|
||||
userinfo.type = _.get(getUserinfo, "value.data.type", "") || "user";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<yl-login-page v-if="!userinfo.login" />
|
||||
<yl-layout v-if="userinfo.login"><router-view></router-view> </yl-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
350
web_vue/src/assets/css/normalize.css
vendored
Normal file
@ -0,0 +1,350 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
/*text-decoration: underline dotted; !* 2 *!*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
44
web_vue/src/assets/css/style.css
Normal file
@ -0,0 +1,44 @@
|
||||
body {
|
||||
font-family: '微软雅黑', serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.menu {
|
||||
height: calc(100vh - 100px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.link:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.space-inline-5 {
|
||||
width: 5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
input {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
.el-main {
|
||||
padding: 5px !important;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.btn-action-div {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
div[aria-hidden='true'] {
|
||||
display: none !important;
|
||||
}
|
BIN
web_vue/src/assets/image/avatar.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
web_vue/src/assets/image/background.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
web_vue/src/assets/image/background.webp
Normal file
After Width: | Height: | Size: 647 KiB |
BIN
web_vue/src/assets/image/black.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
web_vue/src/assets/image/chess.webp
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
web_vue/src/assets/image/logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
web_vue/src/assets/image/white.png
Normal file
After Width: | Height: | Size: 28 KiB |
102
web_vue/src/components/charts/yl-line.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const chartData = defineProps<{
|
||||
xData: Array<string>;
|
||||
yData: Array<{ name: string; value: Array<number> }>;
|
||||
title?: string;
|
||||
per?: boolean;
|
||||
clickable?: boolean;
|
||||
smooth?: boolean;
|
||||
spanLimit?: number;
|
||||
setIndex?: (e: number) => void;
|
||||
defaultIndex?: boolean;
|
||||
}>();
|
||||
const chart = ref();
|
||||
let myChart: echarts.EChartsType;
|
||||
const setOption = () => {
|
||||
const option = {
|
||||
title: {
|
||||
text: chartData.title,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
grid: {
|
||||
left: "10%",
|
||||
right: "5%",
|
||||
top: "10%",
|
||||
bottom: "10%",
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: chartData.xData,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
max: chartData.per ? 100 : "dataMax",
|
||||
axisLabel: {
|
||||
formatter: chartData.per ? "{value}%" : "{value}",
|
||||
},
|
||||
},
|
||||
series: chartData.yData.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
data: e.value,
|
||||
smooth: chartData.smooth,
|
||||
type: "line",
|
||||
};
|
||||
}),
|
||||
dataZoom: {
|
||||
type: "inside",
|
||||
maxValueSpan: chartData.spanLimit || chartData.xData.length,
|
||||
minValueSpan: 5,
|
||||
startValue:
|
||||
chartData.spanLimit && chartData.xData.length > chartData.spanLimit
|
||||
? chartData.xData.length - 1 - chartData.spanLimit
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
myChart.setOption(option);
|
||||
};
|
||||
const init = () => {
|
||||
// const myChart = echarts.init(chart.value);
|
||||
window.onresize = () => myChart.resize();
|
||||
setOption();
|
||||
if (chartData.clickable) {
|
||||
myChart.off("click");
|
||||
myChart.getZr().on("click", function (params) {
|
||||
const pointInPixel = [params.offsetX, params.offsetY];
|
||||
if (myChart && myChart.containPixel("grid", pointInPixel)) {
|
||||
let xIndex = myChart.convertFromPixel({ seriesIndex: 0 }, [
|
||||
params.offsetX,
|
||||
params.offsetY,
|
||||
])[0];
|
||||
if (chartData.setIndex) {
|
||||
chartData.setIndex(xIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!chartData.defaultIndex) {
|
||||
chartData.setIndex?.(chartData.xData.length - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
myChart = echarts.init(chart.value);
|
||||
init();
|
||||
});
|
||||
watch(
|
||||
() => chartData.xData,
|
||||
() => setOption(),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chart" style="width: 100%; height: 100%" v-if="xData.length"></div>
|
||||
<el-empty v-else description="暂无数据"></el-empty>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
14
web_vue/src/components/yl-action-div.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
className?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`btn-action-div ${className}`">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<el-divider style="margin: 0" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
110
web_vue/src/components/yl-hls-player.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="video-container" @keyup.space="togglePlay">
|
||||
<video class="video-element" ref="videoElement" controls></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import Hls from "hls.js";
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const videoElement = ref(null);
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const progress = ref(0);
|
||||
const volume = ref(1);
|
||||
const hls = ref(null);
|
||||
|
||||
const initPlayer = () => {
|
||||
if (Hls.isSupported()) {
|
||||
hls.value = new Hls();
|
||||
hls.value.loadSource(props.src);
|
||||
hls.value.attachMedia(videoElement.value);
|
||||
|
||||
hls.value.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log("HLS 流已解析");
|
||||
});
|
||||
|
||||
hls.value.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error("HLS Error:", data);
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
console.error("网络错误,尝试重新加载");
|
||||
hls.value.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.error("媒体错误,尝试恢复");
|
||||
hls.value.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
hls.value.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (videoElement.value.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
// Safari 原生支持
|
||||
videoElement.value.src = props.src;
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlay = () => {
|
||||
isPlaying.value ? videoElement.value.pause() : videoElement.value.play();
|
||||
};
|
||||
onMounted(() => {
|
||||
initPlayer();
|
||||
|
||||
videoElement.value.addEventListener("loadedmetadata", () => {
|
||||
duration.value = videoElement.value.duration;
|
||||
});
|
||||
|
||||
videoElement.value.addEventListener("timeupdate", () => {
|
||||
currentTime.value = videoElement.value.currentTime;
|
||||
progress.value = (currentTime.value / duration.value) * 100;
|
||||
});
|
||||
|
||||
videoElement.value.addEventListener("play", () => {
|
||||
isPlaying.value = true;
|
||||
});
|
||||
|
||||
videoElement.value.addEventListener("pause", () => {
|
||||
isPlaying.value = false;
|
||||
});
|
||||
|
||||
videoElement.value.addEventListener("volumechange", () => {
|
||||
volume.value = videoElement.value.volume;
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (hls.value) {
|
||||
hls.value.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-container {
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
83
web_vue/src/components/yl-icon-selector.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { iconSelector } from "@/store/store.ts";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
const filter = ref("");
|
||||
const { data: icons, loading } = useRequest(() => myRequest(`/api/sys/icon`), {
|
||||
ready: () => !Boolean(iconSelector.iconList.length),
|
||||
});
|
||||
watch(icons, () => {
|
||||
iconSelector.iconList = icons.value.data.map((e: { icon: string }) => e.icon);
|
||||
});
|
||||
defineEmits<{ (e: "update", value: string): void }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="iconSelector.visible"
|
||||
append-to-body
|
||||
:show-close="false"
|
||||
style="max-width: 60vw; max-height: 60vh; overflow-y: auto"
|
||||
>
|
||||
<template #header>
|
||||
<el-input
|
||||
placeholder="输入名称筛选,双击左键选择图标"
|
||||
clearable
|
||||
v-model="filter"
|
||||
style="
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
background: #fff;
|
||||
width: calc(100% - 5rem);
|
||||
"
|
||||
/>
|
||||
<el-icon><Close @click="iconSelector.visible = false" /></el-icon>
|
||||
</template>
|
||||
|
||||
<div v-loading="loading">
|
||||
<el-icon
|
||||
v-for="item in iconSelector.iconList.filter(
|
||||
(e: string) => e.indexOf(filter) > -1,
|
||||
)"
|
||||
size="32"
|
||||
style="padding: 1rem"
|
||||
:class="{ selected: iconSelector.selected === item }"
|
||||
><el-tooltip :content="item" placement="top"
|
||||
><i
|
||||
:class="{ [item]: true }"
|
||||
@click="
|
||||
iconSelector.selected !== item
|
||||
? (iconSelector.selected = item)
|
||||
: (iconSelector.selected = '')
|
||||
"
|
||||
@dblclick="
|
||||
() => {
|
||||
iconSelector.selected = item;
|
||||
$emit('update', iconSelector.selected);
|
||||
iconSelector.visible = false;
|
||||
}
|
||||
" /></el-tooltip
|
||||
></el-icon>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selected {
|
||||
background-color: #409eff50;
|
||||
}
|
||||
:global(.el-icon:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
:global(.el-dialog__header) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
140
web_vue/src/components/yl-layout/header.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import _ from "lodash";
|
||||
import { SwitchButton } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import { ref } from "vue";
|
||||
import YlUserinfo from "@/components/yl-layout/yl-userinfo.vue";
|
||||
import router from "@/router/router.ts";
|
||||
import { updateMenu } from "@/utils/menu.ts";
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
const { data: weather } = useRequest(() =>
|
||||
myRequest(`/api/weather`, userinfo.token),
|
||||
);
|
||||
const { runAsync: logout } = useRequest(
|
||||
() => myRequest(`/api/user/login`, userinfo.token, {}, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: update } = useRequest(
|
||||
(param: userinfo) => myRequest(`/api/user`, userinfo.token, param, "put"),
|
||||
{ manual: true },
|
||||
);
|
||||
const toLogout = () => {
|
||||
logout()
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
userinfo.login = false;
|
||||
userinfo.token = "";
|
||||
router.push("/");
|
||||
updateMenu();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(formatError(err));
|
||||
userinfo.login = false;
|
||||
userinfo.token = "";
|
||||
});
|
||||
};
|
||||
const toUpdateUserinfo = (data: userinfo) => {
|
||||
update(data)
|
||||
.then(() => {
|
||||
userinfo.userinfo = { ...data };
|
||||
show.value = false;
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex between header" style="height: 100%">
|
||||
<div class="logo">
|
||||
<el-image src="/static/image/logo.png" class="rotate" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-carousel
|
||||
height="60px"
|
||||
autoplay
|
||||
:interval="6000"
|
||||
indicator-position="none"
|
||||
direction="vertical"
|
||||
:pause-on-hover="false"
|
||||
>
|
||||
<el-carousel-item
|
||||
v-for="(item, key) in weather?.data.split(';')"
|
||||
:key="key"
|
||||
>
|
||||
<span>{{ key ? "明" : "今" }}日天气:{{ item }}</span>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div class="user flex around">
|
||||
<el-avatar
|
||||
class="cursor-pointer"
|
||||
:src="
|
||||
_.get(userinfo, 'userinfo.avatar', '') || '/static/image/avatar.png'
|
||||
"
|
||||
@click="show = true"
|
||||
/>
|
||||
<el-tooltip content="注销登录" placement="bottom" effect="light">
|
||||
<el-icon size="20" color="red" class="cursor-pointer" @click="toLogout">
|
||||
<SwitchButton
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog title="修改信息" v-model="show" width="40rem">
|
||||
<YlUserinfo @update="toUpdateUserinfo" :userinfo="userinfo.userinfo" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#cc6aa555,
|
||||
#108ee955,
|
||||
#2dcca755,
|
||||
#cc6aa555,
|
||||
#108ee955
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
overflow: hidden;
|
||||
animation: bg-pan-left 8s infinite both;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
@keyframes bg-pan-left {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
width: 60px;
|
||||
}
|
||||
.rotate {
|
||||
animation: rotate-cw 3s infinite linear both;
|
||||
}
|
||||
@keyframes rotate-cw {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: calc(100vw - 200px);
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
}
|
||||
.user {
|
||||
width: 7rem;
|
||||
}
|
||||
</style>
|
44
web_vue/src/components/yl-layout/layout.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import YlMenu from "@/components/yl-layout/menu.vue";
|
||||
import YlHeader from "@/components/yl-layout/header.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header style="padding: 0">
|
||||
<yl-header></yl-header>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<el-aside style="background-color: antiquewhite; width: fit-content">
|
||||
<yl-menu></yl-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-main style="background-color: beige; height: calc(100vh - 90px)"
|
||||
><el-scrollbar height="calc(100vh - 100px)" class="main-div"
|
||||
><div style="height: calc(100vh - 100px)"><slot></slot></div
|
||||
></el-scrollbar>
|
||||
</el-main>
|
||||
<el-footer class="flex around footer" height="30px">@ ylsa</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.common-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
.footer {
|
||||
font-size: small;
|
||||
font-weight: lighter;
|
||||
}
|
||||
.main-div {
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
border-radius: 7px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
53
web_vue/src/components/yl-layout/menu.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { menuData } from "@/store/store.ts";
|
||||
import router from "@/router/router.ts";
|
||||
const isCollapsed = ref(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-menu
|
||||
class="yl-layout-menu"
|
||||
:collapse="isCollapsed"
|
||||
style="height: 100%"
|
||||
:default-active="menuData.default || 'home'"
|
||||
@dblclick="isCollapsed = !isCollapsed"
|
||||
>
|
||||
<template v-for="item in menuData.data.filter((e) => !e.route_only)">
|
||||
<el-sub-menu
|
||||
v-if="item.detail && item.detail.length > 0"
|
||||
:index="item.menu_id"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon><i :class="item.icon" /></el-icon>
|
||||
<span style="min-width: 6rem">{{ item.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="m in item.detail"
|
||||
:index="m.menu_id"
|
||||
@click="router.push(m.path)"
|
||||
>
|
||||
<el-icon> <i :class="m.icon" /></el-icon>
|
||||
<span style="min-width: 6rem">{{ m.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item
|
||||
v-else
|
||||
:index="item.menu_id"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
<el-icon> <i :class="item.icon" /></el-icon>
|
||||
<span style="min-width: 6rem">{{ item.name }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.yl-layout-menu {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
</style>
|
128
web_vue/src/components/yl-layout/yl-userinfo.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import { UploadProps } from "element-plus";
|
||||
|
||||
const props = defineProps<{ userinfo: userinfo; admin?: boolean }>();
|
||||
|
||||
const emit = defineEmits<{ update: [value: userinfo] }>();
|
||||
|
||||
const locationOption = ref<{ label: string; value: string }[]>([]);
|
||||
const tmpUserinfo = ref<userinfo>({ ...props.userinfo });
|
||||
const tmpUserType = ref<string[]>([]);
|
||||
|
||||
const { runAsync, loading } = useRequest(
|
||||
(param: string) => myRequest(`/api/weather/location?location=${param}`),
|
||||
{ manual: true },
|
||||
);
|
||||
const { data: sys } = useRequest(
|
||||
() => myRequest(`/admin/sys`, userinfo.token),
|
||||
{ ready: () => props.admin },
|
||||
);
|
||||
const getLocation = (query: string) => {
|
||||
runAsync(query).then((res) => {
|
||||
locationOption.value = res.data
|
||||
? Object.keys(res.data).map((key) => ({
|
||||
label: res.data[key],
|
||||
value: key,
|
||||
}))
|
||||
: [];
|
||||
});
|
||||
};
|
||||
const afterAvatarUpload: UploadProps["onSuccess"] = (res) => {
|
||||
tmpUserinfo.value.avatar = res.data;
|
||||
};
|
||||
const toSave = () => emit("update", tmpUserinfo.value);
|
||||
watch(props, () => (tmpUserinfo.value = props.userinfo));
|
||||
watch(sys, () => {
|
||||
if (sys) {
|
||||
const d = sys.value.data.filter(
|
||||
(e: { name: string }) => e.name === "user_type",
|
||||
);
|
||||
console.log(d);
|
||||
if (d.length) {
|
||||
tmpUserType.value = d[0].data.map((e: { value: string }) => e.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form label-width="100px" class="userinfo-edit">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="tmpUserinfo.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input
|
||||
v-model="tmpUserinfo.nickname"
|
||||
placeholder="填写呢称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码">
|
||||
<el-input
|
||||
v-model="tmpUserinfo.mobile"
|
||||
placeholder="填写手机号码"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="tmpUserinfo.email" placeholder="填写邮箱" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型" v-if="props.admin">
|
||||
<el-select v-model="tmpUserinfo.type" placeholder="选择用户类型">
|
||||
<el-option
|
||||
v-for="item in tmpUserType"
|
||||
:label="item"
|
||||
:value="item"
|
||||
:key="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所在地">
|
||||
<el-select
|
||||
v-model="tmpUserinfo.location"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
placeholder="选择所在地"
|
||||
:loading="loading"
|
||||
:remote-method="getLocation"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in locationOption.map((e) => ({
|
||||
value: Object.keys(e)[0],
|
||||
label: Object.values(e)[0],
|
||||
}))"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像"
|
||||
><el-upload
|
||||
:show-file-list="false"
|
||||
action="/api/file/static/avatar"
|
||||
:on-success="afterAvatarUpload"
|
||||
:headers="{ Authorization: `Bearer ${userinfo.token}` }"
|
||||
>
|
||||
<el-avatar
|
||||
:src="tmpUserinfo.avatar"
|
||||
v-if="tmpUserinfo.avatar" /><el-avatar
|
||||
src="/src/assets/image/avatar.png"
|
||||
v-else /></el-upload
|
||||
></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="toSave">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.userinfo-edit {
|
||||
padding-right: 5rem;
|
||||
}
|
||||
</style>
|
24
web_vue/src/main.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { createApp } from "vue";
|
||||
import "./assets/css/normalize.css";
|
||||
import "./assets/css/style.css";
|
||||
import ElementPlus from "element-plus";
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
import "element-plus/dist/index.css";
|
||||
import "animate.css";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import App from "./App.vue";
|
||||
import router from "@/router/router.ts";
|
||||
import { config } from "md-editor-v3";
|
||||
|
||||
config({
|
||||
markdownItPlugins(plugins) {
|
||||
return plugins.map((item) => {
|
||||
if (item.type === "taskList") {
|
||||
return { ...item, options: { ...item.options, enabled: true } };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
createApp(App).use(ElementPlus, { locale: zhCn }).use(router).mount("#app");
|
86
web_vue/src/pages/account-bill/bill-component.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Close } from "@element-plus/icons-vue";
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
import { ref } from "vue";
|
||||
import router from "@/router/router.ts";
|
||||
import { account } from "@/store/store.ts";
|
||||
defineProps<{
|
||||
data?: accountBillData[];
|
||||
loading: boolean;
|
||||
detail?: boolean;
|
||||
}>();
|
||||
const currentIndex = ref(-1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="bill-chart">
|
||||
<yl-line
|
||||
v-if="data"
|
||||
:x-data="data.map((e) => e.date)"
|
||||
:y-data="[{ name: '', value: data.map((e) => e.balance) }]"
|
||||
:set-index="(e) => (currentIndex = e)"
|
||||
:span-limit="12"
|
||||
clickable
|
||||
smooth
|
||||
/>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
显示余额:<el-switch v-model="account.show">
|
||||
<template #active-action><Check /></template>
|
||||
<template #inactive-action><Close /></template>
|
||||
</el-switch>
|
||||
</div>
|
||||
<div class="center" v-if="data?.[currentIndex]">
|
||||
<div>
|
||||
{{ data[currentIndex].duration || data[currentIndex].date }} :
|
||||
<el-button
|
||||
link
|
||||
v-if="detail && data[currentIndex].duration"
|
||||
@click="router.push(`/bill/${data[currentIndex].duration}`)"
|
||||
>{{
|
||||
data[currentIndex].changes === 0
|
||||
? "无变动"
|
||||
: data[currentIndex].changes > 0
|
||||
? `收入 ${account.show ? data[currentIndex].changes : "*****"}`
|
||||
: `支出 ${account.show ? -data[currentIndex].changes : "*****"}`
|
||||
}}</el-button
|
||||
><span v-else-if="data[currentIndex].duration">{{
|
||||
data[currentIndex].changes === 0
|
||||
? "无变动"
|
||||
: data[currentIndex].changes > 0
|
||||
? `收入 ${account.show ? data[currentIndex].changes : "*****"}`
|
||||
: `支出 ${account.show ? -data[currentIndex].changes : "*****"}`
|
||||
}}</span
|
||||
><span v-else>---------</span>
|
||||
</div>
|
||||
<el-table :data="data[currentIndex].detail">
|
||||
<el-table-column prop="card" label="项目" align="center" />
|
||||
<el-table-column prop="balance" label="余额" align="center"
|
||||
><template #default="scope">
|
||||
{{ account.show ? scope.row.balance : "*****" }}
|
||||
</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="changes" label="收支" align="center">
|
||||
<template #default="scope">
|
||||
{{
|
||||
scope.row.changes === 0
|
||||
? "-----"
|
||||
: scope.row.changes > 0
|
||||
? `收入 ${account.show ? scope.row.changes : "*****"}`
|
||||
: `支出 ${account.show ? -scope.row.changes : "*****"}`
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bill-chart {
|
||||
padding: 1rem 1rem;
|
||||
height: 20rem;
|
||||
}
|
||||
</style>
|
20
web_vue/src/pages/account-bill/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import BillComponent from "@/pages/account-bill/bill-component.vue";
|
||||
|
||||
const { data, loading } = useRequest<{ data?: accountBillData[] }>(
|
||||
() => myRequest(`/api/yeb_log`, userinfo.token),
|
||||
{
|
||||
refreshDeps: [() => userinfo.token],
|
||||
ready: () => Boolean(userinfo.token),
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BillComponent :loading="loading" :data="data?.data" detail />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
32
web_vue/src/pages/account-bill/month.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { useRoute } from "vue-router";
|
||||
import BillComponent from "@/pages/account-bill/bill-component.vue";
|
||||
import router from "@/router/router.ts";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { data, loading } = useRequest<{ data?: accountBillData[] }>(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/yeb_log/detail?start=${(route.params.date as string).split("~")[0]}&end=${(route.params.date as string).split("~")[1]}`,
|
||||
userinfo.token,
|
||||
),
|
||||
{
|
||||
refreshDeps: [() => userinfo.token],
|
||||
ready: () => Boolean(userinfo.token),
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="router.push('/bill')">返回月视图</el-button>
|
||||
</yl-action-div>
|
||||
<BillComponent :loading="loading" :data="data?.data" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
262
web_vue/src/pages/account/index.vue
Normal file
@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { ref } from "vue";
|
||||
import _ from "lodash";
|
||||
import { account, userinfo } from "@/store/store.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import { Check, Close } from "@element-plus/icons-vue";
|
||||
import { sum } from "lodash-es";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const editData = ref({ card: "", type: false, balance: 0 });
|
||||
const dialog = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const editYe = ref(false);
|
||||
// const show = ref(false);
|
||||
|
||||
const { data, loading, run } = useRequest<{
|
||||
data: { balance: number; type: boolean; card: string }[];
|
||||
}>(() => myRequest(`/api/yeb`, userinfo.token));
|
||||
const { runAsync: updateCard } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/yeb`,
|
||||
userinfo.token,
|
||||
{ ...editData.value, balance: Number(editData.value.balance) },
|
||||
"put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: deleteCard } = useRequest(
|
||||
(card: string) =>
|
||||
myRequest(`/api/yeb`, userinfo.token, { card: card }, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: addCard, loading: adding } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/yeb`,
|
||||
userinfo.token,
|
||||
{ ...editData.value, balance: Number(editData.value.balance) },
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const addCardBtn = () => {
|
||||
editData.value = { card: "", type: false, balance: 0 };
|
||||
dialog.value = true;
|
||||
};
|
||||
const addCardFunc = () => {
|
||||
if (
|
||||
!editData.value.card ||
|
||||
(!editData.value.balance && editData.value.balance !== 0)
|
||||
) {
|
||||
ElMessage.error("参数格式错误");
|
||||
return;
|
||||
}
|
||||
addCard()
|
||||
.then((res) => {
|
||||
editData.value = { card: "", type: false, balance: 0 };
|
||||
ElMessage.success(res.data);
|
||||
run();
|
||||
dialog.value = false;
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
const updateCardFunc = () => {
|
||||
updateCard()
|
||||
.then((res) => {
|
||||
ElMessage.success(res.data);
|
||||
run();
|
||||
editData.value = { card: "", type: false, balance: 0 };
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
const deleteCardFunc = (e: string) => {
|
||||
deleteCard(e)
|
||||
.then((res) => {
|
||||
ElMessage.success(res.data);
|
||||
run();
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
const updateType = (
|
||||
t: boolean,
|
||||
d: { card: string; type: boolean; balance: number },
|
||||
) => {
|
||||
editData.value = { ...d, type: t };
|
||||
updateCard()
|
||||
.then((res) => {
|
||||
ElMessage.success(res.data);
|
||||
run();
|
||||
editData.value = { card: "", type: false, balance: 0 };
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="addCardBtn">新增项目</el-button>
|
||||
</yl-action-div>
|
||||
<el-table
|
||||
stripe
|
||||
v-loading="loading"
|
||||
:data="
|
||||
_.get(data, 'data', []).filter(
|
||||
(_: {}, index: number) =>
|
||||
index >= (page - 1) * pageSize && index < page * pageSize,
|
||||
)
|
||||
"
|
||||
>
|
||||
<el-table-column prop="card" label="项目" align="center" />
|
||||
<el-table-column prop="balance" label="余额(双击修改)" align="center">
|
||||
<template #default="scope"
|
||||
><div
|
||||
v-if="scope.row.card !== editData.card"
|
||||
class="balance-div"
|
||||
:style="`color:${scope.row.type ? 'red' : ''}`"
|
||||
@dblclick="
|
||||
() => {
|
||||
editYe = true;
|
||||
editData = {
|
||||
card: scope.row.card,
|
||||
type: scope.row.type,
|
||||
balance: scope.row.balance,
|
||||
};
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ account.show ? scope.row.balance : "******" }}
|
||||
</div>
|
||||
<div v-else-if="editYe" class="flex between">
|
||||
<el-input
|
||||
v-model="editData.balance"
|
||||
type="number"
|
||||
placeholder="输入当前余额"
|
||||
:min="0"
|
||||
:max="99999999"
|
||||
style="width: calc(100% - 50px)"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
<div v-if="editYe">
|
||||
<el-icon color="green" size="20"
|
||||
><i class="bi-check2" @click="updateCardFunc"
|
||||
/></el-icon>
|
||||
<el-icon
|
||||
color="red"
|
||||
size="20"
|
||||
@click="editData = { card: '', type: false, balance: 0 }"
|
||||
><i class="bi-x"
|
||||
/></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支出型" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.type"
|
||||
inline-prompt
|
||||
:loading="loading"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
@change="
|
||||
(t: boolean) => {
|
||||
editYe = false;
|
||||
updateType(t, scope.row);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-popconfirm
|
||||
:title="`确认删除 ${scope.row.card} ?`"
|
||||
cancel-button-text="取消"
|
||||
confirm-button-text="确认"
|
||||
@confirm="() => deleteCardFunc(scope.row.card)"
|
||||
><template #reference
|
||||
><el-icon color="red"><Close /></el-icon></template
|
||||
></el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex between pagination-div">
|
||||
<div>
|
||||
<label>显示余额:</label
|
||||
><el-switch v-model="account.show">
|
||||
<template #active-action><Check /></template>
|
||||
<template #inactive-action><Close /></template> </el-switch
|
||||
><label v-if="account.show">
|
||||
合计:{{
|
||||
sum(data?.data.map((e) => (e.type ? -e.balance : e.balance))).toFixed(
|
||||
2,
|
||||
)
|
||||
}}</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<el-pagination
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="_.get(data, 'data', []).length"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:hide-on-single-page="pageSize === 10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog title="新增项目" v-model="dialog">
|
||||
<div class="input-div flex between">
|
||||
<el-input v-model="editData.card" placeholder="输入项目名称" clearable>
|
||||
<template #prefix>
|
||||
<el-switch
|
||||
v-model="editData.type"
|
||||
active-text="借贷"
|
||||
inactive-text="存款"
|
||||
style="--el-switch-off-color: #606266; --el-switch-on-color: red"
|
||||
inline-prompt
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="input-div">
|
||||
<el-input
|
||||
v-model="editData.balance"
|
||||
type="number"
|
||||
placeholder="输入当前余额"
|
||||
:min="0"
|
||||
:max="99999999"
|
||||
><template #prefix>¥</template></el-input
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="addCardFunc" :disabled="adding"
|
||||
>保存</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-div {
|
||||
margin: 1rem 2rem;
|
||||
}
|
||||
.dialog-footer {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
.balance-div:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.pagination-div {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
87
web_vue/src/pages/account/yl-account-sz.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
|
||||
const szData = ref({ type: true, card: "", amount: 0 });
|
||||
|
||||
const { data: cardList } = useRequest<{
|
||||
data: { balance: number; type: boolean; card: string }[];
|
||||
}>(() => myRequest(`/api/yeb`, userinfo.token));
|
||||
const { runAsync: updateCard, loading } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/yeb/sz`,
|
||||
userinfo.token,
|
||||
{ ...szData.value, amount: Number(szData.value.amount) },
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const update = () => {
|
||||
updateCard()
|
||||
.then(() => {
|
||||
szData.value.amount = 0;
|
||||
ElMessage.success("成功");
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
|
||||
watch(cardList, () => {
|
||||
szData.value.card = cardList.value?.data[0]?.card || "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form :model="szData" class="sz">
|
||||
<el-form-item prop="szType">
|
||||
<el-radio-group v-model="szData.type">
|
||||
<el-radio :value="false">收入</el-radio>
|
||||
<el-radio :value="true">支出</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="card">
|
||||
<el-select v-model="szData.card">
|
||||
<el-option
|
||||
v-for="item in cardList?.data"
|
||||
:label="item.card"
|
||||
:value="item.card"
|
||||
>
|
||||
<el-text :type="item.type ? 'danger' : ''">{{ item.card }}</el-text>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="amount">
|
||||
<el-input
|
||||
v-model="szData.amount"
|
||||
type="number"
|
||||
placeholder="输入当前余额"
|
||||
:min="0"
|
||||
:max="99999999"
|
||||
style="width: calc(100% - 50px)"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="update" :disabled="loading"
|
||||
>提交</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sz {
|
||||
background-color: #eeea;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
:global(.el-form-item__content) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
13
web_vue/src/pages/error/index.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import router from "@/router/router.js";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-result title="404" sub-title="页面无法访问" icon="error">
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="() => router.back()">返回</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
231
web_vue/src/pages/files/index.vue
Normal file
@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowRight, HomeFilled } from "@element-plus/icons-vue";
|
||||
import { useFileStore } from "@/pages/files/store.ts";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage, UploadFile, UploadUserFile } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import router from "@/router/router.ts";
|
||||
|
||||
const fileIcon = [
|
||||
{ type: "dir", icon: "bi-folder" },
|
||||
{ type: "zip", icon: "bi-file-zip" },
|
||||
{ type: "image", icon: "bi-image" },
|
||||
{ type: "audio", icon: "bi-music-note-beamed" },
|
||||
{ type: "video", icon: "bi-camera-video" },
|
||||
{ type: "doc", icon: "bi-file-earmark" },
|
||||
];
|
||||
const newFolder = ref({ visible: false, name: "" });
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
const imgPreview = ref({ visible: false, src: "" });
|
||||
|
||||
const { data, loading, run } = useRequest<{
|
||||
data: { dirs: string[] | null; files: fileList[] | null };
|
||||
}>(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api_file/file/upload${useFileStore.path.length > 0 ? "/" + useFileStore.path.join("/") : ""}`,
|
||||
userinfo.token,
|
||||
),
|
||||
{ refreshDeps: [() => useFileStore.path] },
|
||||
);
|
||||
const { runAsync: addFolder } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api_file/file/upload/${useFileStore.path
|
||||
.concat([newFolder.value.name])
|
||||
.join("/")}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const { runAsync: delFile } = useRequest(
|
||||
(name: string) =>
|
||||
myRequest(
|
||||
`/api_file/file/upload/${useFileStore.path.concat([name]).join("/")}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"delete",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const toPath = (index: number) => {
|
||||
useFileStore.path = useFileStore.path.filter((_, i) => i <= index);
|
||||
};
|
||||
const funcAddFolder = () => {
|
||||
addFolder()
|
||||
.then(() => {
|
||||
newFolder.value = { visible: false, name: "" };
|
||||
run();
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const funcDelFile = (file: string) => {
|
||||
delFile(file)
|
||||
.then(() => {
|
||||
run();
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const funcDownloadFile = (file: string) => {
|
||||
const win = window.open("_black");
|
||||
const url = `/api_file/file/download/${useFileStore.path.concat([file]).join("/")}`;
|
||||
if (win) {
|
||||
win.location.href = url;
|
||||
}
|
||||
};
|
||||
const handleFileUpload = (file: UploadFile) => {
|
||||
if (file.status === "success") {
|
||||
fileList.value = fileList.value.filter((e) => e.name !== file.name);
|
||||
run();
|
||||
} else if (file.status === "fail") {
|
||||
ElMessage.error(`${file.name}上传失败`);
|
||||
}
|
||||
};
|
||||
watch(fileList, () => console.log(fileList));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 面包屑路由-->
|
||||
<el-breadcrumb :separator-icon="ArrowRight" style="padding: 0.5rem">
|
||||
<el-breadcrumb-item @click="() => (useFileStore.path = [])"
|
||||
><el-icon size="20"><home-filled /></el-icon
|
||||
></el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="(item, index) in useFileStore.path"
|
||||
><el-tooltip :content="item" placement="top" effect="light"
|
||||
><span class="path-item cursor-pointer" @click="() => toPath(index)">{{
|
||||
item
|
||||
}}</span></el-tooltip
|
||||
></el-breadcrumb-item
|
||||
>
|
||||
</el-breadcrumb>
|
||||
<!-- 分割线,新建文件夹,上传文件-->
|
||||
<div class="divider">
|
||||
<div style="background: #fff">
|
||||
<el-button link @click="newFolder = { visible: true, name: '' }"
|
||||
><el-icon><i class="bi-folder-plus" /></el-icon
|
||||
><span> 新建文件夹</span></el-button
|
||||
><el-upload
|
||||
:action="`/api_file/file/upload-file/${useFileStore.path.length ? useFileStore.path.join('/') : 'root(根目录上传的文件)'}`"
|
||||
multiple
|
||||
:headers="{ Authorization: 'Bearer ' + userinfo.token }"
|
||||
name="file"
|
||||
:file-list="fileList"
|
||||
:on-change="handleFileUpload"
|
||||
><el-button link
|
||||
><el-icon><i class="bi-upload" /></el-icon
|
||||
><span> 上传文件</span></el-button
|
||||
></el-upload
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件列表-->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
size="small"
|
||||
v-if="data"
|
||||
:data="
|
||||
data?.data.dirs
|
||||
? data.data.dirs
|
||||
.map((e) => ({ name: e, type: 'dir' }))
|
||||
.concat(data.data.files || [])
|
||||
: data.data.files
|
||||
"
|
||||
:row-class-name="(d: any) => (d.row.type === 'dir' ? 'cursor-pointer' : '')"
|
||||
@row-dblclick="
|
||||
(r: fileList) =>
|
||||
r.type === 'dir'
|
||||
? (useFileStore.path = useFileStore.path.concat([r.name]))
|
||||
: 1
|
||||
"
|
||||
>
|
||||
<el-table-column label="名称" prop="name">
|
||||
<template #default="scope">
|
||||
<el-icon size="15"
|
||||
><i
|
||||
:class="`${fileIcon.filter((e) => e.type === scope.row.type)[0].icon}`"
|
||||
/></el-icon>
|
||||
<span>{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大小" prop="size" align="center" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-icon
|
||||
v-if="scope.row.type === 'image'"
|
||||
@click="
|
||||
() =>
|
||||
(imgPreview = {
|
||||
visible: true,
|
||||
src: `/api_file/file/download/${useFileStore.path
|
||||
.concat([scope.row.name])
|
||||
.join('/')}`,
|
||||
})
|
||||
"
|
||||
><i class="bi-eye"
|
||||
/></el-icon>
|
||||
<el-icon
|
||||
v-if="scope.row.type === 'video'"
|
||||
@click="
|
||||
router.push(
|
||||
`/file/video?file=${useFileStore.path.concat([scope.row.name]).join('/')}`,
|
||||
)
|
||||
"
|
||||
><i class="bi-play-btn"
|
||||
/></el-icon>
|
||||
<el-icon
|
||||
color="#108ee9"
|
||||
@click="() => funcDownloadFile(scope.row.name)"
|
||||
v-if="scope.row.type !== 'dir'"
|
||||
><i class="bi-download"
|
||||
/></el-icon>
|
||||
<el-popconfirm
|
||||
:title="`确认删除 ${scope.row.name} ?`"
|
||||
confirm-button-text="确认"
|
||||
cancel-button-text="取消"
|
||||
@confirm="() => funcDelFile(scope.row.name)"
|
||||
><template #reference
|
||||
><el-icon color="red"><i class="bi-trash" /></el-icon></template
|
||||
></el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 新建文件夹弹窗-->
|
||||
<el-dialog title="新建文件夹" v-model="newFolder.visible">
|
||||
<el-input placeholder="输入文件夹名称" v-model="newFolder.name" clearable />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="funcAddFolder">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-image-viewer
|
||||
:url-list="[imgPreview.src]"
|
||||
v-if="imgPreview.visible"
|
||||
@close="imgPreview.visible = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.path-item {
|
||||
display: table-cell;
|
||||
max-width: 10rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 20px;
|
||||
}
|
||||
.divider {
|
||||
text-align: center;
|
||||
padding: 0 35%;
|
||||
background: linear-gradient(#fff calc(50% - 1px), #0003, #fff 50%);
|
||||
}
|
||||
i {
|
||||
padding: 0 0.2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
5
web_vue/src/pages/files/store.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { reactive } from "vue";
|
||||
|
||||
export const useFileStore = reactive<{ path: string[] }>({
|
||||
path: [],
|
||||
});
|
37
web_vue/src/pages/files/yl-video.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import router from "@/router/router.js";
|
||||
import { useRoute } from "vue-router";
|
||||
import YlHlsPlayer from "@/components/yl-hls-player.vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
const route = useRoute();
|
||||
const { data: videoCheck, loading } = useRequest(() =>
|
||||
myRequest(`/api_file/file/download-video-check/${route.query.file}`),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-button link @click="router.push(`/file`)" class="back"
|
||||
><el-icon><i class="bi-arrow-left-circle" /></el-icon
|
||||
><span> 返回</span></el-button
|
||||
>
|
||||
<div v-loading="loading" class="video">
|
||||
<YlHlsPlayer
|
||||
v-if="videoCheck.data.video && videoCheck.data.m3u8"
|
||||
:src="`/api/file/download-video/${route.query.file}`"
|
||||
/>
|
||||
<video controls v-else style="height: 100%; width: 100%">
|
||||
<source :src="`/api/file/download/${route.query.file}`" />
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.video {
|
||||
width: 100%;
|
||||
height: calc(100vh - 140px);
|
||||
}
|
||||
</style>
|
152
web_vue/src/pages/games/yl-backgammon-room.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import _ from "lodash";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { data: roomStatus, run } = useRequest(
|
||||
() => myRequest(`/api/backgammon/${_.get(route, "params.id", "")}`),
|
||||
{ pollingInterval: 1000, refreshOnWindowFocus: true },
|
||||
);
|
||||
|
||||
const { runAsync: joinRoom } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/backgammon/${_.get(route, "params.id", "")}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const { runAsync: addPawn } = useRequest(
|
||||
(p: string) =>
|
||||
myRequest(
|
||||
`/api/backgammon/${_.get(route, "params.id", "")}?place=${p}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const { runAsync: leaveRoom } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/backgammon/${_.get(route, "params.id", "")}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"delete",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const add = (x: number, y: number) => {
|
||||
if (_.get(roomStatus, "value.data.winner", "")) {
|
||||
ElMessage.error("对局已结束");
|
||||
return;
|
||||
}
|
||||
addPawn(`${x},${y}`)
|
||||
.then(() => run())
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
|
||||
const toJoin = () => {
|
||||
joinRoom()
|
||||
.then()
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
|
||||
const toLeave = () => {
|
||||
leaveRoom()
|
||||
.then(() => {})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="toLeave">退出</el-button>
|
||||
<el-button
|
||||
v-if="_.get(roomStatus, 'data.player', '').split(',').length < 2"
|
||||
link
|
||||
@click="toJoin"
|
||||
>加入对战
|
||||
</el-button>
|
||||
<div v-if="_.get(roomStatus, 'data.winner', '')" class="winner">
|
||||
winner:{{ _.get(roomStatus, "data.winner", "") }}
|
||||
</div>
|
||||
</yl-action-div>
|
||||
<div class="gameDiv">
|
||||
<div class="player">
|
||||
{{ _.get(roomStatus, "data.player", "").split(",")[0] }}
|
||||
</div>
|
||||
<div class="canvas">
|
||||
<div
|
||||
v-for="(row, col) in _.get(roomStatus, 'data.pawn_status', '').split(
|
||||
';',
|
||||
)"
|
||||
class="flex between"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in row.split(',')"
|
||||
:class="{ black: item === '1', white: item === '-1' }"
|
||||
style="width: 50px; height: 50px"
|
||||
@click="() => add(col, index)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player">
|
||||
{{ _.get(roomStatus, "data.player", "").split(",")[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gameDiv {
|
||||
height: 90%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.black {
|
||||
background: url("/src/assets/image/black.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: url("/src/assets/image/white.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
background: url("/src/assets/image/background.jpg") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
font-size: 1px;
|
||||
width: 750px;
|
||||
height: 750px;
|
||||
border: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.winner {
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
font-size: 20px;
|
||||
color: #108ee9;
|
||||
}
|
||||
|
||||
.player {
|
||||
font-weight: bolder;
|
||||
font-size: 18px;
|
||||
color: #ff79ce;
|
||||
}
|
||||
</style>
|
68
web_vue/src/pages/games/yl-backgammon.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import _ from "lodash";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { data: rooms, run } = useRequest(() => myRequest(`/api/backgammon`), {
|
||||
pollingInterval: 3000,
|
||||
});
|
||||
const { runAsync: addRoom } = useRequest(
|
||||
() => myRequest(`/api/backgammon`, userinfo.token, {}, "post"),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const toRoom = (id: number) => {
|
||||
router.push(`/backgammon/${id}`);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
addRoom()
|
||||
.then(() => run())
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="onAdd">新建房间</el-button>
|
||||
</yl-action-div>
|
||||
<div class="rooms">
|
||||
<div
|
||||
v-for="room in _.get(rooms, 'data', []) as roomStatus[]"
|
||||
class="roomDiv"
|
||||
>
|
||||
<div class="status" @click="() => toRoom(room.room_id as number)">
|
||||
{{ room.player.split(",")[0] }}<span> vs </span
|
||||
>{{ room.player.split(",")[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.roomDiv {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
margin: 2rem;
|
||||
height: 8rem;
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
||||
.roomDiv:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
padding-top: 2rem;
|
||||
color: #ff79ce;
|
||||
}
|
||||
</style>
|
285
web_vue/src/pages/games/yl-chess-room.vue
Normal file
@ -0,0 +1,285 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ref, watch } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import _ from "lodash";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const pawns =
|
||||
"車馬相仕帥仕相馬車炮炮兵兵兵兵兵車馬象士将士象馬車砲砲卒卒卒卒卒";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const current = ref("");
|
||||
const { data, run } = useRequest(
|
||||
() => myRequest(`/api/chess/${route.params.id}`),
|
||||
{
|
||||
pollingInterval: 1000,
|
||||
},
|
||||
);
|
||||
const { runAsync: join } = useRequest(
|
||||
() => myRequest(`/api/chess/${route.params.id}`, userinfo.token, {}, "post"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: leave } = useRequest(
|
||||
() =>
|
||||
myRequest(`/api/chess/${route.params.id}`, userinfo.token, {}, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: reset } = useRequest(
|
||||
() =>
|
||||
myRequest(`/api/chess/${route.params.id}/reset`, userinfo.token, {}, "get"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: update } = useRequest(
|
||||
(to: string) =>
|
||||
myRequest(
|
||||
`/api/chess/${route.params.id}?pawn=${current.value}&des=${to}`,
|
||||
userinfo.token,
|
||||
{},
|
||||
"put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: addAi } = useRequest(
|
||||
() =>
|
||||
myRequest(`/api/chess/${route.params.id}/ai`, userinfo.token, {}, "post"),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: getAiStep } = useRequest(
|
||||
() =>
|
||||
myRequest(`/api/chess/${route.params.id}/ai`, userinfo.token, {}, "put"),
|
||||
{ manual: true },
|
||||
);
|
||||
const status = ref<string[]>([]);
|
||||
|
||||
const leaveRoom = () => {
|
||||
if (
|
||||
_.get(data, "value.data.players", "")
|
||||
.split(",")
|
||||
.indexOf(userinfo.username) !== -1
|
||||
) {
|
||||
leave()
|
||||
.then((res) => {
|
||||
if ((_.get(res, "code", 0) as number) === 200) {
|
||||
router.back();
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
const joinRoom = () => {
|
||||
join()
|
||||
.then((res) => {
|
||||
if ((_.get(res, "code", 0) as number) === 200) {
|
||||
run();
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const resetRoom = () => {
|
||||
reset()
|
||||
.then((res) => {
|
||||
if ((_.get(res, "code", 0) as number) === 200) {
|
||||
run();
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const updatePawn = (i: number, j: number) => {
|
||||
(_.get(data, "value.data.players", "").split(",")[0] === userinfo.username
|
||||
? update(`${9 - i},${j}`)
|
||||
: update(`${i},${j}`)
|
||||
)
|
||||
.then((res) => {
|
||||
if ((_.get(res, "code", 0) as number) === 200) {
|
||||
current.value = "";
|
||||
run();
|
||||
if (
|
||||
_.get(data, "value.data.players", "").split(",")[1] ===
|
||||
"ai--" + route.params.id
|
||||
) {
|
||||
getAiStep()
|
||||
.then((r) => {
|
||||
if ((_.get(r, "code", 0) as number) === 200) {
|
||||
run();
|
||||
}
|
||||
})
|
||||
.catch((r) => ElMessage.error(formatError(r)));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const addChessAi = () => {
|
||||
addAi()
|
||||
.then((res) => {
|
||||
if ((_.get(res, "code", 0) as number) === 200) {
|
||||
run();
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
|
||||
watch(data, () => {
|
||||
status.value =
|
||||
_.get(data, "value.data.players", "").split(",")[0] === userinfo.username
|
||||
? _.get(data, "value.data.status", "").split(";").reverse()
|
||||
: _.get(data, "value.data.status", "").split(";");
|
||||
});
|
||||
watch(
|
||||
() => _.get(data, "value.data.current", ""),
|
||||
() => (current.value = ""),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="leaveRoom">退出房间</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
_.get(data, 'data.players', '')
|
||||
.split(',')
|
||||
.indexOf(userinfo.username) === -1
|
||||
"
|
||||
link
|
||||
@click="joinRoom"
|
||||
>加入房间
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="_.get(data, 'data.players', '').split(',')[0] === userinfo.username"
|
||||
link
|
||||
@click="resetRoom"
|
||||
>重置房间
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="_.get(data, 'data.players', '') === userinfo.username"
|
||||
link
|
||||
@click="addChessAi"
|
||||
>添加机器人
|
||||
</el-button>
|
||||
</yl-action-div>
|
||||
<div v-if="_.get(data, 'data.winner', '')" class="winner">
|
||||
winner: {{ _.get(data, "data.winner", "") }}
|
||||
</div>
|
||||
<div class="flex around" style="padding-top: 20px">
|
||||
<div
|
||||
:class="{
|
||||
current:
|
||||
_.get(data, 'data.current', '') ===
|
||||
_.get(data, 'data.players', '').split(',')[0],
|
||||
}"
|
||||
class="red player"
|
||||
>
|
||||
{{ _.get(data, "data.players", "").split(",")[0] || "" }}
|
||||
</div>
|
||||
<div class="chess-div">
|
||||
<div v-for="(row, i) in status" class="flex around">
|
||||
<div
|
||||
v-for="(col, j) in row.split(',')"
|
||||
class="pawn-div"
|
||||
@click="
|
||||
() =>
|
||||
current !== '-1' && current !== '' && current !== col
|
||||
? updatePawn(i, j)
|
||||
: current === col ||
|
||||
(Number(col) > 15 &&
|
||||
_.get(data, 'data.players', '').split(',')[0] ===
|
||||
userinfo.username) ||
|
||||
(Number(col) < 16 &&
|
||||
_.get(data, 'data.players', '').split(',')[1] ===
|
||||
userinfo.username)
|
||||
? (current = '')
|
||||
: (current = col)
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="{
|
||||
pawn: col !== '-1',
|
||||
red: Number(col) < 16,
|
||||
black: Number(col) > 15,
|
||||
selected: col === current,
|
||||
}"
|
||||
>
|
||||
{{ col !== "-1" ? pawns[Number(col)] : "" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
current:
|
||||
_.get(data, 'data.current', '') ===
|
||||
_.get(data, 'data.players', '').split(',')[1],
|
||||
}"
|
||||
class="black player"
|
||||
>
|
||||
{{ _.get(data, "data.players", "").split(",")[1] || "" }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chess-div {
|
||||
height: 700px;
|
||||
width: 604px;
|
||||
border: #2dcca7 1px solid;
|
||||
background: #cc6aa510 url("/src/assets/image/chess.webp") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.pawn-div {
|
||||
margin: 10px 9px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
line-height: 45px;
|
||||
font-family: "楷体", serif;
|
||||
font-size: 35px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.pawn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 60px;
|
||||
border: #f0c237 double 1px;
|
||||
box-shadow: -5px 5px 5px #f0c237ee;
|
||||
background-color: #f6e3a8ee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.player {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ff0000aa;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000000aa;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: aquamarine;
|
||||
}
|
||||
|
||||
.current {
|
||||
border: dotted 1px #b78eff;
|
||||
}
|
||||
|
||||
.winner {
|
||||
text-align: center;
|
||||
color: #108ee9;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
</style>
|
73
web_vue/src/pages/games/yl-chess.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import _ from "lodash";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { data: rooms, run: refresh } = useRequest(
|
||||
() => myRequest(`/api/chess`),
|
||||
{ pollingInterval: 1000 },
|
||||
);
|
||||
const { runAsync: newRoom } = useRequest(
|
||||
() => myRequest(`/api/chess`, userinfo.token, {}, "post"),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const createRoom = () => {
|
||||
newRoom().then((res) => {
|
||||
if (_.get(res, "code", 0) === 200) {
|
||||
refresh();
|
||||
router.push(`/chess/${_.get(res, "data", "")}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button link @click="refresh">刷新</el-button>
|
||||
<el-button link @click="createRoom">新建房间</el-button>
|
||||
</yl-action-div>
|
||||
<div class="rooms">
|
||||
<div
|
||||
v-for="item in _.get(rooms, 'data', []) as chess[]"
|
||||
class="room"
|
||||
@click="router.push(`/chess/${item.ID}`)"
|
||||
>
|
||||
<div>no.{{ item.ID }}</div>
|
||||
<div class="players flex around">
|
||||
<span>{{ item.players?.split(",")[0] || "-" }}</span> vs
|
||||
<span>{{ item.players?.split(",")[1] || "-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.room {
|
||||
width: 20%;
|
||||
display: inline-block;
|
||||
margin: 1rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #2dcca729;
|
||||
text-align: center;
|
||||
height: 5rem;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.players {
|
||||
font-size: 1.5rem;
|
||||
|
||||
span:first-child {
|
||||
color: #ff0000aa;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: #000000aa;
|
||||
}
|
||||
}
|
||||
</style>
|
70
web_vue/src/pages/home/index.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
// import YlIconSelector from "@/components/yl-icon-selector.vue";
|
||||
import { message, userinfo } from "@/store/store.ts";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { DateTime, Duration } from "luxon";
|
||||
import { getCookie } from "@/utils/cookie.ts";
|
||||
import YlAccountSz from "@/pages/account/yl-account-sz.vue";
|
||||
|
||||
const loginTime = ref(
|
||||
Math.floor(new Date().getTime() - (Number(getCookie("loginTime")) || 0)),
|
||||
);
|
||||
const currentTime = ref(new Date().getTime());
|
||||
let down: NodeJS.Timeout;
|
||||
|
||||
onMounted(() => {
|
||||
down = setInterval(() => {
|
||||
loginTime.value = Math.floor(
|
||||
new Date().getTime() - (Number(getCookie("loginTime")) || 0),
|
||||
);
|
||||
currentTime.value = new Date().getTime();
|
||||
}, 1000);
|
||||
});
|
||||
onUnmounted(() => clearInterval(down));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="center">
|
||||
welcome,{{ userinfo.userinfo.nickname || userinfo.username }}
|
||||
</h1>
|
||||
<el-row>
|
||||
<el-col style="text-align: center" :xs="24" :sm="8">
|
||||
<el-statistic title="当前在线人数" :value="message.online"></el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="8" style="text-align: center" :xs="24" :sm="8">
|
||||
<el-statistic
|
||||
title="已登录时长"
|
||||
:value="loginTime"
|
||||
:formatter="
|
||||
(v: number) =>
|
||||
Duration.fromObject({ millisecond: v }).toFormat('MM-dd hh:mm:ss')
|
||||
"
|
||||
></el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="8" style="text-align: center" :xs="24" :sm="8">
|
||||
<el-statistic
|
||||
title="当前时间"
|
||||
:value="currentTime"
|
||||
:formatter="
|
||||
(v: number) => DateTime.fromMillis(v).toFormat('yyyy-MM-dd HH:mm:ss')
|
||||
"
|
||||
></el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row justify="space-around">
|
||||
<el-col
|
||||
style="text-align: center; max-width: 450px"
|
||||
class="account"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<YlAccountSz />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account {
|
||||
padding: 20px 50px;
|
||||
}
|
||||
</style>
|
49
web_vue/src/pages/login/index.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import YlLogin from "@/pages/login/login.vue";
|
||||
import YlRegister from "@/pages/login/register.vue";
|
||||
import { loginData } from "@/store/store.js";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="background">
|
||||
<div class="login-div">
|
||||
<yl-login
|
||||
:class="{
|
||||
animate__animated: true,
|
||||
animate__fadeInLeft: loginData.login,
|
||||
animate__fadeOutLeft: !loginData.login,
|
||||
}"
|
||||
></yl-login>
|
||||
<yl-register
|
||||
:class="{
|
||||
animate__animated: true,
|
||||
animate__fadeInRight: !loginData.login,
|
||||
animate__fadeOutRight: loginData.login,
|
||||
}"
|
||||
></yl-register>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.background {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: url("@/assets/image/background.webp") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.login-div {
|
||||
width: 30rem;
|
||||
height: 35rem;
|
||||
background-color: #ffffff19;
|
||||
border-radius: 1rem;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
box-shadow: 10px 10px 15px #ffffff30;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
67
web_vue/src/pages/login/login-data.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { loginData } from "@/store/store.ts";
|
||||
import { Lock, User } from "@element-plus/icons-vue";
|
||||
defineProps<{
|
||||
showReInput?: boolean;
|
||||
}>();
|
||||
const clearLoginData = () => {
|
||||
loginData.username = "";
|
||||
loginData.password = "";
|
||||
loginData.repeatPass = "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-div">
|
||||
<el-input
|
||||
v-model="loginData.username"
|
||||
placeholder="输入用户名"
|
||||
size="large"
|
||||
clearable
|
||||
autofocus
|
||||
class="input"
|
||||
@clear="clearLoginData"
|
||||
><template #prefix>
|
||||
<el-icon><User /></el-icon></template
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="loginData.password"
|
||||
placeholder="输入密码"
|
||||
size="large"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="input"
|
||||
><template #prefix>
|
||||
<el-icon><Lock /></el-icon> </template
|
||||
></el-input>
|
||||
<el-input
|
||||
v-if="showReInput"
|
||||
v-model="loginData.repeatPass"
|
||||
placeholder="重复输入密码"
|
||||
size="large"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="input"
|
||||
><template #prefix>
|
||||
<el-icon><Lock /></el-icon> </template
|
||||
></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-div {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.input {
|
||||
margin: 2rem;
|
||||
height: 3rem;
|
||||
width: calc(100% - 8rem);
|
||||
}
|
||||
.input > :global(.el-input__wrapper) {
|
||||
background-color: #e8f0fe !important;
|
||||
border-radius: 2rem !important;
|
||||
}
|
||||
</style>
|
134
web_vue/src/pages/login/login.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { loginData, userinfo } from "@/store/store.js";
|
||||
import YlLoginData from "@/pages/login/login-data.vue";
|
||||
import YlResetPwd from "@/pages/login/reset-pwd.vue";
|
||||
import { CaretRight } from "@element-plus/icons-vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import jsEncrypt from "jsencrypt";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import { getCookie } from "@/utils/cookie.ts";
|
||||
const loading = ref(false);
|
||||
const { runAsync: getPubKey } = useRequest(
|
||||
() => myRequest(`/api/user/login?username=${loginData.username}`),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: login } = useRequest(
|
||||
(enPass: string) =>
|
||||
myRequest(
|
||||
`/api/user/login?username=${loginData.username}&password=${encodeURIComponent(enPass)}&auto=${loginData.autoLogin}`,
|
||||
"",
|
||||
{},
|
||||
"post",
|
||||
),
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const toLogin = () => {
|
||||
if (!loginData.username) {
|
||||
ElMessage.error("获取公钥失败,请填写用户名");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
getPubKey()
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
const encrypt = new jsEncrypt();
|
||||
encrypt.setPublicKey(res.data as string);
|
||||
const enPwd = encrypt.encrypt(loginData.password);
|
||||
login(enPwd as string)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.code === 200) {
|
||||
userinfo.login = true;
|
||||
userinfo.token = res.data as string;
|
||||
}
|
||||
setInterval(
|
||||
() => {
|
||||
!getCookie("token") && window.location.reload();
|
||||
},
|
||||
1000 * 60 * 60,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
ElMessage.error(`登录失败:${formatError(err)}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
ElMessage.error(`获取公钥失败:${formatError(err)}`);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: absolute; width: 100%; height: 100%">
|
||||
<div class="title">用户登录</div>
|
||||
<yl-login-data />
|
||||
<div class="forget">
|
||||
<el-checkbox
|
||||
v-model="loginData.autoLogin"
|
||||
label="自动登录"
|
||||
size="large"
|
||||
></el-checkbox>
|
||||
<yl-reset-pwd />
|
||||
</div>
|
||||
<div class="action-div">
|
||||
<el-button
|
||||
link
|
||||
size="large"
|
||||
@click="loginData.login = false"
|
||||
:disabled="loading"
|
||||
>转到注册<el-icon><CaretRight /></el-icon
|
||||
></el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="toLogin"
|
||||
:disabled="loading"
|
||||
>{{ loading ? "登录中。。。" : "登录" }}</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(180deg, #ff69b4, #ff69b450);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.forget {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin: 2rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
.action-div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
height: calc(100% - 27rem);
|
||||
}
|
||||
:global(.el-button.is-link) {
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
107
web_vue/src/pages/login/register.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { loginData, userinfo } from "@/store/store.js";
|
||||
import YlLoginData from "@/pages/login/login-data.vue";
|
||||
import { CaretLeft } from "@element-plus/icons-vue";
|
||||
import { ref } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import jsEncrypt from "jsencrypt";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
const loading = ref(false);
|
||||
const { runAsync: getPubKey } = useRequest(
|
||||
() => myRequest(`/api/user/login?username=${loginData.username}`),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: runRegister } = useRequest(
|
||||
(enPass: string) =>
|
||||
myRequest(
|
||||
`/api/user?username=${loginData.username}&password=${encodeURIComponent(enPass)}`,
|
||||
"",
|
||||
{},
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
const toRegister = () => {
|
||||
if (!loginData.username) {
|
||||
ElMessage.error("请输入用户名");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
getPubKey()
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
const encrypt = new jsEncrypt();
|
||||
encrypt.setPublicKey(res.data as string);
|
||||
const enPwd = encrypt.encrypt(loginData.password);
|
||||
runRegister(enPwd as string)
|
||||
.then((r) => {
|
||||
loading.value = false;
|
||||
if (r.code === 200) {
|
||||
userinfo.login = true;
|
||||
userinfo.username = loginData.username;
|
||||
userinfo.token = r.data;
|
||||
}
|
||||
})
|
||||
.catch((r) => {
|
||||
loading.value = false;
|
||||
ElMessage.error(`登录失败:${formatError(r)}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((res) => {
|
||||
loading.value = false;
|
||||
ElMessage.error(`获取公钥失败:${formatError(res)}`);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: absolute; width: 100%; height: 100%">
|
||||
<div class="title">用户注册</div>
|
||||
<yl-login-data show-re-input />
|
||||
<div class="action-div">
|
||||
<el-button link size="large" @click="loginData.login = true"
|
||||
><el-icon><CaretLeft /></el-icon>返回登录</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="
|
||||
!loginData.username ||
|
||||
!loginData.password ||
|
||||
loginData.password !== loginData.repeatPass ||
|
||||
loading
|
||||
"
|
||||
@click="toRegister"
|
||||
>
|
||||
{{ loading ? "注册中。。。" : "注册" }}</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(180deg, #ff69b4, #ff69b450);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.action-div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
height: calc(100% - 27rem);
|
||||
}
|
||||
</style>
|
190
web_vue/src/pages/login/reset-pwd.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { loginData, userinfo } from "@/store/store.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError, rsaEncrypt } from "@/utils/utils.ts";
|
||||
import { Lock } from "@element-plus/icons-vue";
|
||||
|
||||
const active = ref(0);
|
||||
const visible = ref(false);
|
||||
const email = ref("");
|
||||
const leftTime = ref(0);
|
||||
const inputData = ref({ confirmCode: "", password: "", rePass: "" });
|
||||
|
||||
const { runAsync: getConfirmCode } = useRequest(
|
||||
() => myRequest(`/api/user/reset?username=${loginData.username}`),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: getPubKey } = useRequest(
|
||||
() => myRequest(`/api/user/login?username=${loginData.username}`),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: runLogin } = useRequest(
|
||||
(enPass: string) =>
|
||||
myRequest(
|
||||
`/api/user/login?username=${loginData.username}&password=${encodeURIComponent(enPass)}&confirmCode=${inputData.value.confirmCode}`,
|
||||
"",
|
||||
{},
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const forgetPwd = () => {
|
||||
if (!loginData.username) {
|
||||
ElMessage.error("请输入用户名");
|
||||
return;
|
||||
}
|
||||
getConfirmCode()
|
||||
.then((res) => {
|
||||
visible.value = true;
|
||||
leftTime.value = 30;
|
||||
email.value = res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(formatError(err));
|
||||
});
|
||||
};
|
||||
|
||||
const resetPwd = () => {
|
||||
getPubKey()
|
||||
.then((res) => {
|
||||
const enPass = rsaEncrypt(
|
||||
res.data as string,
|
||||
inputData.value.password as string,
|
||||
);
|
||||
runLogin(enPass as string)
|
||||
.then((r) => {
|
||||
visible.value = false;
|
||||
ElMessage.success("密码重置完成");
|
||||
userinfo.login = true;
|
||||
userinfo.token = r.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(formatError(err));
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(formatError(err));
|
||||
});
|
||||
};
|
||||
|
||||
watch(visible, () => {
|
||||
active.value = 0;
|
||||
});
|
||||
let timer: any;
|
||||
watch(leftTime, () => {
|
||||
if (leftTime.value === 30) {
|
||||
timer = setInterval(() => {
|
||||
leftTime.value--;
|
||||
}, 1000);
|
||||
}
|
||||
if (leftTime.value === 0) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-button link @click="forgetPwd">忘记密码 ?</el-button>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="密码重置"
|
||||
append-to-body
|
||||
style="min-width: 30rem"
|
||||
>
|
||||
<el-steps :active="active" align-center>
|
||||
<el-step title="邮箱验证" />
|
||||
<el-step title="重置密码" />
|
||||
</el-steps>
|
||||
<div class="content" v-if="active === 0">
|
||||
<div class="email-div flex around">
|
||||
验证码已发送邮箱:{{ email }}, 5分钟内有效
|
||||
</div>
|
||||
<div class="confirm-div flex around">
|
||||
<div class="flex" style="width: 15rem">
|
||||
<label style="width: 5rem">验证码:</label>
|
||||
<el-input
|
||||
v-model="inputData.confirmCode"
|
||||
clearable
|
||||
placeholder="请填写验证码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{{ leftTime > 0 ? `${leftTime}s 后` : ""
|
||||
}}<el-button link :disabled="leftTime > 0" @click="forgetPwd"
|
||||
>重新发送</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-if="active === 1">
|
||||
<div class="input-div">
|
||||
<el-input
|
||||
v-model="inputData.password"
|
||||
placeholder="输入密码"
|
||||
size="large"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="input"
|
||||
><template #prefix>
|
||||
<el-icon><Lock /></el-icon> </template
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="inputData.rePass"
|
||||
placeholder="重复输入密码"
|
||||
size="large"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
class="input"
|
||||
><template #prefix>
|
||||
<el-icon><Lock /></el-icon> </template
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button link @click="active--" v-if="active > 0">上一步</el-button>
|
||||
<el-button
|
||||
@click="active++"
|
||||
v-if="active === 0"
|
||||
:disabled="!inputData.confirmCode"
|
||||
>下一步</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="active > 0"
|
||||
:disabled="
|
||||
!inputData.password || inputData.rePass !== inputData.password
|
||||
"
|
||||
@click="resetPwd"
|
||||
>完成</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.email-div {
|
||||
height: 5rem;
|
||||
}
|
||||
.confirm-div {
|
||||
height: 5rem;
|
||||
min-width: 25rem;
|
||||
max-width: 30rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.input-div {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.input {
|
||||
margin: 2rem;
|
||||
height: 3rem;
|
||||
width: calc(100% - 8rem);
|
||||
}
|
||||
</style>
|
45
web_vue/src/pages/notes/store.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { reactive } from "vue";
|
||||
|
||||
export const useNotes: {
|
||||
id: string;
|
||||
content: string;
|
||||
preview: boolean;
|
||||
toolbars: any[];
|
||||
} = reactive({
|
||||
id: "my-editor",
|
||||
content: "",
|
||||
preview: true,
|
||||
toolbars: [
|
||||
"bold",
|
||||
"underline",
|
||||
"italic",
|
||||
"-",
|
||||
"title",
|
||||
"strikeThrough",
|
||||
"sub",
|
||||
"sup",
|
||||
"quote",
|
||||
"unorderedList",
|
||||
"orderedList",
|
||||
"task",
|
||||
"-",
|
||||
"codeRow",
|
||||
"code",
|
||||
"link",
|
||||
"image",
|
||||
"table",
|
||||
"mermaid",
|
||||
"katex",
|
||||
"-",
|
||||
"revoke",
|
||||
"next",
|
||||
"save",
|
||||
0,
|
||||
"=",
|
||||
"pageFullscreen",
|
||||
"fullscreen",
|
||||
"preview",
|
||||
"previewOnly",
|
||||
"catalog",
|
||||
],
|
||||
});
|
134
web_vue/src/pages/notes/yl-note-detail.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { MdCatalog, MdEditor, MdPreview, NormalToolbar } from "md-editor-v3";
|
||||
import "md-editor-v3/lib/preview.css";
|
||||
import "md-editor-v3/lib/style.css";
|
||||
import { useNotes } from "@/pages/notes/store.ts";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { onMounted, watch } from "vue";
|
||||
import _ from "lodash";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import router from "@/router/router.ts";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
let scrollElement = document.getElementsByClassName("yl-editor-preview")[0];
|
||||
const route = useRoute();
|
||||
|
||||
const {
|
||||
data: note,
|
||||
loading,
|
||||
run,
|
||||
} = useRequest(() =>
|
||||
myRequest(`/api/notes/${route.params.id}`, userinfo.token),
|
||||
);
|
||||
|
||||
const { runAsync: save } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/notes/${route.params.id}`,
|
||||
userinfo.token,
|
||||
{ id: Number(route.params.id), content: useNotes.content },
|
||||
"put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const toSave = () => {
|
||||
save()
|
||||
.then(() => {
|
||||
useNotes.preview = true;
|
||||
run();
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
|
||||
const onUploadImg = async (files: File[], callback: (r: string[]) => void) => {
|
||||
const res = (await Promise.all(
|
||||
files.map((file) => {
|
||||
return new Promise((rev, rej) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
axios
|
||||
.post(`/api/notes/${route.params.id}/file`, form, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
Authorization: `Bearer ${userinfo.token}`,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
rev(res.data.data);
|
||||
})
|
||||
.catch((error) => rej(error));
|
||||
});
|
||||
}),
|
||||
)) as string[];
|
||||
|
||||
callback(res);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
scrollElement = document.getElementsByClassName("yl-editor-preview")[0];
|
||||
});
|
||||
|
||||
watch(note, () => {
|
||||
useNotes.content = _.get(note.value, "data.content", "");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="useNotes.preview">
|
||||
<yl-action-div>
|
||||
<el-button link @click="router.push('/notes')">返回</el-button>
|
||||
</yl-action-div>
|
||||
</div>
|
||||
<div
|
||||
class="yl-editor-preview"
|
||||
v-if="useNotes.preview"
|
||||
v-loading="loading"
|
||||
@dblclick="() => (useNotes.preview = false)"
|
||||
>
|
||||
<MdPreview :editor-id="useNotes.id" :model-value="useNotes.content" />
|
||||
<MdCatalog
|
||||
:editor-id="useNotes.id"
|
||||
:scroll-element="scrollElement as HTMLElement"
|
||||
/>
|
||||
</div>
|
||||
<MdEditor
|
||||
:editor-id="useNotes.id"
|
||||
v-else
|
||||
v-loading="loading"
|
||||
auto-focus
|
||||
v-model="useNotes.content"
|
||||
@onUploadImg="onUploadImg"
|
||||
@onSave="toSave"
|
||||
style="height: 100%"
|
||||
:toolbars="useNotes.toolbars"
|
||||
><template #defToolbars>
|
||||
<normal-toolbar title="退出" @on-click="() => (useNotes.preview = true)">
|
||||
<template #trigger>
|
||||
<el-icon
|
||||
class="md-editor-icon"
|
||||
aria-hidden="true"
|
||||
style="padding: 4px"
|
||||
>
|
||||
<i class="bi-box-arrow-right" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</normal-toolbar> </template
|
||||
></MdEditor>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.yl-editor-preview {
|
||||
display: flex;
|
||||
height: calc(100% - 55px);
|
||||
.md-editor-catalog {
|
||||
width: 20%;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
102
web_vue/src/pages/notes/yl-note.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { MdPreview } from "md-editor-v3";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import _ from "lodash";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
data: noteList,
|
||||
loading,
|
||||
run,
|
||||
} = useRequest(() => myRequest(`/api/notes`, userinfo.token), {
|
||||
refreshDeps: [() => userinfo.token],
|
||||
});
|
||||
|
||||
const { runAsync: add } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/notes`,
|
||||
userinfo.token,
|
||||
{ content: "# new note \n" },
|
||||
"post",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
const { runAsync: del } = useRequest(
|
||||
(id: number) => myRequest(`/api/notes/${id}`, userinfo.token, {}, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const toDetail = (id: number) => {
|
||||
router.push(`/notes/${id}`);
|
||||
};
|
||||
const toAdd = () => {
|
||||
add()
|
||||
.then((res) => toDetail(_.get(res, "data", -1)))
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
const toDelete = (id: number) => {
|
||||
ElMessageBox.confirm(`确认删除笔记--${id}?`, "确认操作", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
del(id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
run();
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div class-name="center">
|
||||
<el-button link @click="toAdd"
|
||||
><el-icon><i class="bi-file-earmark-plus" /></el-icon
|
||||
><span>新建笔记</span></el-button
|
||||
>
|
||||
</yl-action-div>
|
||||
<div v-loading="loading">
|
||||
<div class="note-div" v-for="item in noteList?.data">
|
||||
<el-icon class="del-icon" @click="() => toDelete(item.id)"
|
||||
><i class="bi-x-circle"
|
||||
/></el-icon>
|
||||
<MdPreview
|
||||
:model-value="item.content"
|
||||
style="text-align: center; height: 100%"
|
||||
@click="toDetail(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.note-div {
|
||||
position: relative;
|
||||
height: 250px;
|
||||
background-color: #2dcca730;
|
||||
padding: 10px 30px 10px 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.note-div:hover {
|
||||
.del-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.del-icon {
|
||||
display: none;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
color: #fab6b6;
|
||||
font-size: 16px;
|
||||
z-index: 9;
|
||||
}
|
||||
</style>
|
22
web_vue/src/pages/sys-info/yl-cpu-chart.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
|
||||
defineProps<{
|
||||
xData: string[];
|
||||
data: number[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 200px; margin-bottom: 2rem">
|
||||
<yl-line
|
||||
:x-data="xData"
|
||||
:y-data="[{ name: 'cpu', value: data }]"
|
||||
per
|
||||
smooth
|
||||
title="CPU使用率"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
22
web_vue/src/pages/sys-info/yl-disk-chart.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
|
||||
defineProps<{
|
||||
xData: string[];
|
||||
data: { point: string; per: number[] }[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 200px; margin-bottom: 2rem">
|
||||
<yl-line
|
||||
:x-data="xData"
|
||||
:y-data="data.map((e) => ({ name: e.point, value: e.per }))"
|
||||
per
|
||||
smooth
|
||||
title="硬盘使用率"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
22
web_vue/src/pages/sys-info/yl-mem-chart.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
|
||||
defineProps<{
|
||||
xData: string[];
|
||||
data: number[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 200px; margin-bottom: 2rem">
|
||||
<yl-line
|
||||
:x-data="xData"
|
||||
:y-data="[{ name: '内存', value: data }]"
|
||||
per
|
||||
smooth
|
||||
title="内存使用率"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
25
web_vue/src/pages/sys-info/yl-net-chart.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
import { transNum } from "@/utils/utils.ts";
|
||||
|
||||
defineProps<{
|
||||
xData: string[];
|
||||
data: { sent: string; rec: string }[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 200px; margin-bottom: 2rem">
|
||||
<yl-line
|
||||
:x-data="xData"
|
||||
:y-data="[
|
||||
{ name: '上传', value: data.map((e) => transNum(e.sent, 'KB')) },
|
||||
{ name: '下载', value: data.map((e) => transNum(e.rec, 'KB')) },
|
||||
]"
|
||||
smooth
|
||||
title="网速(KB/s)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
122
web_vue/src/pages/sys-info/yl-sys-info.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { DateTime } from "luxon";
|
||||
import YlCpuChart from "@/pages/sys-info/yl-cpu-chart.vue";
|
||||
import YlMemChart from "@/pages/sys-info/yl-mem-chart.vue";
|
||||
import YlDiskChart from "@/pages/sys-info/yl-disk-chart.vue";
|
||||
import YlNetChart from "@/pages/sys-info/yl-net-chart.vue";
|
||||
|
||||
const date = ref<Date>(new Date());
|
||||
const selectIp = ref("");
|
||||
|
||||
const { data: systems, loading: systems_loading } = useRequest<resSystem>(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/sys_info/server?date=${DateTime.fromJSDate(date.value).toFormat("yyyy-LL-dd")}`,
|
||||
userinfo.token,
|
||||
),
|
||||
{ refreshDeps: [date] },
|
||||
);
|
||||
|
||||
const { data: sys_info, loading: info_loading } = useRequest<resSysInfo>(
|
||||
() =>
|
||||
myRequest(
|
||||
`/api/sys_info?date=${DateTime.fromJSDate(date.value).toFormat("yyyy-LL-dd")}&ip=${selectIp.value}`,
|
||||
userinfo.token,
|
||||
),
|
||||
{
|
||||
ready: () => Boolean(selectIp.value),
|
||||
refreshDeps: [date, selectIp],
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex between" style="height: 100%">
|
||||
<div class="side-menu">
|
||||
<div>服务器列表</div>
|
||||
<div v-loading="systems_loading">
|
||||
<div
|
||||
v-for="item in systems?.data"
|
||||
:class="{ select: item.ip === selectIp }"
|
||||
@click="selectIp = item.ip"
|
||||
>
|
||||
{{ item.hostname }}--{{ item.ip }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="content" style="height: 100%; overflow-y: auto">
|
||||
<div class="date-div">
|
||||
日期:<el-date-picker
|
||||
v-model="date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="(d: Date) => d > new Date()"
|
||||
/>
|
||||
</div>
|
||||
<div v-loading="info_loading">
|
||||
<yl-cpu-chart
|
||||
v-if="sys_info?.data?.length"
|
||||
:data="sys_info?.data.map((e) => e.cpu_per)"
|
||||
:x-data="sys_info?.data.map((e) => e.datetime)"
|
||||
/>
|
||||
<yl-mem-chart
|
||||
v-if="sys_info?.data?.length"
|
||||
:data="sys_info?.data.map((e) => e.mem.mem_per)"
|
||||
:x-data="sys_info?.data.map((e) => e.datetime)"
|
||||
/>
|
||||
<yl-disk-chart
|
||||
v-if="sys_info?.data?.length"
|
||||
:data="
|
||||
sys_info
|
||||
? sys_info.data[0].disk.map((e, i) => ({
|
||||
point: e.point,
|
||||
per: sys_info?.data.map(
|
||||
(item) => item.disk[i].per,
|
||||
) as number[],
|
||||
}))
|
||||
: []
|
||||
"
|
||||
:x-data="sys_info?.data.map((e) => e.datetime)"
|
||||
/>
|
||||
<yl-net-chart
|
||||
v-if="sys_info?.data?.length"
|
||||
:data="sys_info?.data.map((e) => e.net)"
|
||||
:x-data="sys_info?.data.map((e) => e.datetime)"
|
||||
/>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.side-menu {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: #2dcca755;
|
||||
text-align: center;
|
||||
div {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.select {
|
||||
background-color: #108ee933;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: 100%;
|
||||
width: calc(100% - 150px);
|
||||
text-align: center;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.date-div {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
z-index: 9;
|
||||
}
|
||||
</style>
|
45
web_vue/src/pages/sys-log/store.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { reactive } from "vue";
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
title: "时间",
|
||||
dataIndex: "time",
|
||||
key: "time",
|
||||
width: 150,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "方法",
|
||||
dataIndex: "method",
|
||||
key: "method",
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "路径",
|
||||
dataIndex: "path",
|
||||
key: "path",
|
||||
align: "center",
|
||||
overflow: true,
|
||||
},
|
||||
{
|
||||
title: "返回码",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "userAgent",
|
||||
dataIndex: "user_agent",
|
||||
key: "user_agent",
|
||||
width: "450",
|
||||
align: "center",
|
||||
overflow: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const sysLog = reactive({
|
||||
date: "",
|
||||
ip: "",
|
||||
});
|
117
web_vue/src/pages/sys-log/yl-docker-log.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const time = ref<[Date | string, Date | string]>(["", ""]);
|
||||
const container = ref<string>("");
|
||||
const message = ref<string>("");
|
||||
const tableData = ref<
|
||||
{
|
||||
time: string;
|
||||
container_name: string;
|
||||
message: string;
|
||||
}[]
|
||||
>([]);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
const { data: containerList } = useRequest(() =>
|
||||
myRequest(`/admin/logs/log-container-list`, userinfo.token),
|
||||
);
|
||||
const { data, run, loading } = useRequest<{
|
||||
data: { time: string; container_name: string; message: string }[];
|
||||
message: number;
|
||||
}>(() =>
|
||||
myRequest(
|
||||
`/admin/logs/sys?start_time=${time.value[0] || ""}&end_time=${time.value[1] || ""}&container_name=${container.value}&message=${message.value}`,
|
||||
userinfo.token,
|
||||
),
|
||||
);
|
||||
watch([page, pageSize], () => {
|
||||
if (data.value) {
|
||||
tableData.value = data?.value.data.filter(
|
||||
(_, i) =>
|
||||
i >= (page.value - 1) * pageSize.value &&
|
||||
i < page.value * pageSize.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
watch(data, () => {
|
||||
if (data.value) {
|
||||
tableData.value = data?.value.data.filter(
|
||||
(_, i) =>
|
||||
i >= (page.value - 1) * pageSize.value &&
|
||||
i < page.value * pageSize.value,
|
||||
);
|
||||
}
|
||||
page.value = 1;
|
||||
pageSize.value = 10;
|
||||
});
|
||||
watch(time, () => console.log(time.value[0], time.value[1]));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div class-name="flex between">
|
||||
<el-select
|
||||
style="width: 200px"
|
||||
v-model="container"
|
||||
clearable
|
||||
@clear="container = ''"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in containerList?.data"
|
||||
:key="item"
|
||||
:value="item"
|
||||
:label="item"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<div>
|
||||
<el-date-picker
|
||||
v-model="time"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@clear="() => (time = ['', ''])"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="message"
|
||||
style="width: 400px"
|
||||
placeholder="请输入查询关键字"
|
||||
/>
|
||||
<el-button type="primary" @click="run">查询</el-button>
|
||||
</yl-action-div>
|
||||
<el-scrollbar class="table">
|
||||
<el-table :data="tableData" v-loading="loading">
|
||||
<el-table-column prop="time" label="时间" width="250px" align="center" />
|
||||
<el-table-column
|
||||
prop="container_name"
|
||||
label="容器名称"
|
||||
width="150px"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="message" label="日志信息" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="flex around" style="margin-top: 20px">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="data?.data.length"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
hide-on-single-page
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table {
|
||||
max-height: calc(100% - 150px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
107
web_vue/src/pages/sys-log/yl-nginx-log-detail.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { columns, sysLog } from "@/pages/sys-log/store.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
|
||||
const { data, loading } = useRequest<{
|
||||
data: { ip: string; location: string }[];
|
||||
}>(() => myRequest(`/admin/logs/detail?date=${sysLog.date}`, userinfo.token), {
|
||||
refreshDeps: () => sysLog.date,
|
||||
});
|
||||
const detailLine = ref<
|
||||
{ name: string; value: number; detail: { ip: string; location: string }[] }[]
|
||||
>([]);
|
||||
const ipIndex = ref(-1);
|
||||
const modal = ref(false);
|
||||
const page = ref(1);
|
||||
const setIp = (index: number) => {
|
||||
ipIndex.value = index;
|
||||
modal.value = true;
|
||||
};
|
||||
watch(data, () => {
|
||||
if (data.value) {
|
||||
detailLine.value = Array.from(
|
||||
new Set(data.value.data.map((e) => e.ip)),
|
||||
).map((e) => ({
|
||||
name: e,
|
||||
value: data.value?.data.filter((item) => item.ip === e).length || 0,
|
||||
detail: data.value?.data.filter((item) => item.ip === e) || [],
|
||||
}));
|
||||
}
|
||||
});
|
||||
watch(modal, () => {
|
||||
page.value = 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="select-date">详情日期:{{ sysLog.date }}</div>
|
||||
<div class="date-view" v-if="detailLine && data" v-loading="loading">
|
||||
<yl-line
|
||||
:x-data="detailLine.map((e) => e.name)"
|
||||
:y-data="[{ name: '', value: detailLine.map((e) => e.value) }]"
|
||||
:set-index="setIp"
|
||||
clickable
|
||||
default-index
|
||||
smooth
|
||||
/>
|
||||
</div>
|
||||
<el-dialog v-model="modal" style="width: 1300px" append-to-body>
|
||||
<div class="title">
|
||||
<label>IP地址:{{ detailLine[ipIndex].name }}</label>
|
||||
<div class="inline-space"></div>
|
||||
<label>地址:{{ detailLine[ipIndex].detail[0].location }}</label>
|
||||
</div>
|
||||
<div class="flex around">
|
||||
<el-pagination
|
||||
hide-on-single-page
|
||||
layout="prev, pager, next"
|
||||
:total="detailLine[ipIndex].detail.length || 0"
|
||||
:current-page="page"
|
||||
@update:current-page="(p: number) => (page = p)"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
:data="
|
||||
detailLine[ipIndex].detail.filter(
|
||||
(_, i) => i >= (page - 1) * 10 && i < page * 10,
|
||||
)
|
||||
"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="item in columns"
|
||||
:prop="item.dataIndex"
|
||||
:label="item.title"
|
||||
:width="item.width"
|
||||
align="center"
|
||||
:show-overflow-tooltip="item.overflow"
|
||||
/>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.select-date {
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
color: #f0c237;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.inline-space {
|
||||
width: 25px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.date-view {
|
||||
padding: 10px 20px;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
38
web_vue/src/pages/sys-log/yl-nginx-log.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { sysLog } from "@/pages/sys-log/store.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import YlLine from "@/components/charts/yl-line.vue";
|
||||
import YlSysLogDetail from "@/pages/sys-log/yl-nginx-log-detail.vue";
|
||||
|
||||
const { data, loading } = useRequest<{
|
||||
data: { name: string; value: number }[];
|
||||
}>(() => myRequest(`/admin/logs`, userinfo.token));
|
||||
const chgDate = (index: number) => {
|
||||
sysLog.date = data?.value ? data.value.data.map((e) => e.name)[index] : "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="date-view" v-loading="loading">
|
||||
<yl-line
|
||||
v-if="data?.data && data.data.length"
|
||||
:x-data="data.data.map((e) => e.name)"
|
||||
:y-data="[{ name: '', value: data.data.map((e) => e.value) }]"
|
||||
:set-index="chgDate"
|
||||
clickable
|
||||
smooth
|
||||
:span-limit="30"
|
||||
/>
|
||||
<el-empty v-else />
|
||||
<yl-sys-log-detail v-if="sysLog.date" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.date-view {
|
||||
padding: 10px 20px;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
91
web_vue/src/pages/sys-log/yl-sys-log.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import YlNginxLog from "@/pages/sys-log/yl-nginx-log.vue";
|
||||
import YlDockerLog from "@/pages/sys-log/yl-docker-log.vue";
|
||||
|
||||
const logType = ref("docker");
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "docker",
|
||||
value: "docker",
|
||||
icon: "/docker_icon.svg",
|
||||
},
|
||||
{
|
||||
label: "nginx",
|
||||
value: "nginx",
|
||||
icon: "/nginx_icon.svg",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex between btn-action-div">
|
||||
<h3>系统运行日志</h3>
|
||||
<el-segmented v-model="logType" :options="options">
|
||||
<template #default="scope">
|
||||
<div class="segmented">
|
||||
<el-icon size="20">
|
||||
<el-image :src="scope.item.icon" />
|
||||
</el-icon>
|
||||
<div>{{ scope.item.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-segmented>
|
||||
</div>
|
||||
<div class="con">
|
||||
<div
|
||||
:class="{
|
||||
animate__animated: true,
|
||||
animate__fadeInLeft: logType === 'docker',
|
||||
animate__fadeOutLeft: logType !== 'docker',
|
||||
}"
|
||||
class="docker"
|
||||
>
|
||||
<yl-docker-log />
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
animate__animated: true,
|
||||
animate__fadeInRight: logType !== 'docker',
|
||||
animate__fadeOutRight: logType === 'docker',
|
||||
}"
|
||||
class="nginx"
|
||||
>
|
||||
<yl-nginx-log />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
color: #415aff;
|
||||
}
|
||||
.segmented {
|
||||
padding: 0.5rem;
|
||||
width: 5rem;
|
||||
}
|
||||
.con {
|
||||
width: 100%;
|
||||
height: calc(100% - 100px);
|
||||
background-color: #ffffff19;
|
||||
border-radius: 0;
|
||||
top: 100px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.docker {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
.nginx {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
214
web_vue/src/pages/sys-menu/index.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import MenuEdit from "@/pages/sys-menu/menu-edit.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
import { updateMenu } from "@/utils/menu.ts";
|
||||
// 空菜单信息
|
||||
const blackMenu: sysMenuList = {
|
||||
route_only: false,
|
||||
menu_id: "",
|
||||
name: "",
|
||||
path: "",
|
||||
icon: "bi-list-ul",
|
||||
white_list: "",
|
||||
user_type: "admin",
|
||||
};
|
||||
const tableData = ref<sysMenuList[]>(); //菜单表格数据
|
||||
const page = ref(1); // 分页当前页数
|
||||
const pageSize = ref(10); // 分页当前页面行数
|
||||
const dialog = ref({
|
||||
visible: false,
|
||||
title: "",
|
||||
}); // 弹窗标题
|
||||
const dialogData = ref<sysMenuList>(blackMenu); // 弹窗菜单数据
|
||||
const expandList = ref<string[]>([]);
|
||||
// 获取菜单列表
|
||||
const {
|
||||
data: menus,
|
||||
loading: loading1,
|
||||
run,
|
||||
} = useRequest<{ data: sysMenuList[] }>(
|
||||
() => myRequest(`/admin/menus`, userinfo.token),
|
||||
{
|
||||
refreshDeps: () => userinfo.token,
|
||||
ready: () => Boolean(userinfo.token),
|
||||
},
|
||||
);
|
||||
// 删除菜单
|
||||
const { runAsync: deleteMenu } = useRequest(
|
||||
(id: string) =>
|
||||
myRequest(`/admin/menus?menu_id=${id}`, userinfo.token, {}, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
// 刷新菜单数据
|
||||
const update = () => {
|
||||
run();
|
||||
dialog.value.visible = false;
|
||||
updateMenu();
|
||||
};
|
||||
//删除菜单方法
|
||||
const deleteMenuItem = (id: string) => {
|
||||
deleteMenu(id)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
run();
|
||||
ElMessage.success("删除成功");
|
||||
}
|
||||
})
|
||||
.catch((res) => ElMessage.error(formatError(res)));
|
||||
};
|
||||
// 菜单根据id转换为二级菜单
|
||||
const getId = (data: sysMenuList) => {
|
||||
for (let i = 1; i <= 99; i++) {
|
||||
const id = data.menu_id + (i < 10 ? "0" + i : i);
|
||||
if (data.children?.filter((e) => e.menu_id === id).length === 0) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
// 根据菜单信息转换菜单为二级菜单数据
|
||||
watch(menus, () => {
|
||||
tableData.value = menus?.value?.data
|
||||
.filter((item) => item.menu_id.length === 3)
|
||||
.map((e) => ({
|
||||
...e,
|
||||
children: menus?.value?.data.filter(
|
||||
(i) => i.menu_id.startsWith(e.menu_id) && i.menu_id !== e.menu_id,
|
||||
),
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div class-name="flex between">
|
||||
<div>
|
||||
<el-button @click="() => run()"
|
||||
><el-icon><i class="bi-arrow-clockwise" /></el-icon
|
||||
><span> 刷新</span></el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加菜单';
|
||||
dialogData = { ...blackMenu };
|
||||
}
|
||||
"
|
||||
>添加菜单</el-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="expandList.length === 0"
|
||||
@click="
|
||||
tableData ? (expandList = tableData?.map((e) => e.menu_id)) : []
|
||||
"
|
||||
>全部展开</el-button
|
||||
><el-button v-else @click="expandList = []">全部折叠</el-button>
|
||||
</div>
|
||||
</yl-action-div>
|
||||
<el-table
|
||||
v-loading="loading1"
|
||||
:data="
|
||||
tableData?.filter(
|
||||
(_: object, i: number) =>
|
||||
i >= (page - 1) * pageSize && i < page * pageSize,
|
||||
)
|
||||
"
|
||||
:expand-row-keys="expandList"
|
||||
@expand-change="
|
||||
(row: sysMenuList, ex: boolean) => {
|
||||
if (ex) {
|
||||
expandList.push(row.menu_id);
|
||||
} else {
|
||||
expandList = expandList.filter((e) => e !== row.menu_id);
|
||||
}
|
||||
}
|
||||
"
|
||||
row-key="menu_id"
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="menu_id" label="ID" align="center" />
|
||||
<el-table-column prop="name" label="名称" align="center" />
|
||||
<el-table-column prop="icon" label="图标" align="center">
|
||||
<template #default="scope">
|
||||
<el-icon size="20"><i :class="scope.row.icon"></i></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="地址" align="center" />
|
||||
<el-table-column prop="route_only" label="仅路由模式" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.route_only" disabled
|
||||
/></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="user_type" label="用户权限" align="center" />
|
||||
<el-table-column prop="white_list" label="白名单" align="center" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
v-if="scope.row.menu_id.length === 3"
|
||||
@click="
|
||||
() => {
|
||||
dialog.title = '添加菜单';
|
||||
dialog.visible = true;
|
||||
dialogData = {
|
||||
...blackMenu,
|
||||
menu_id: getId(scope.row),
|
||||
icon: scope.row.icon,
|
||||
};
|
||||
}
|
||||
"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
@click="
|
||||
() => {
|
||||
dialog.title = '修改菜单';
|
||||
dialog.visible = true;
|
||||
dialogData = { ...scope.row };
|
||||
}
|
||||
"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-popconfirm
|
||||
title="确认删除菜单?"
|
||||
@confirm="() => deleteMenuItem(scope.row.menu_id)"
|
||||
>
|
||||
<template #reference
|
||||
><el-button type="danger" size="small">删除</el-button></template
|
||||
>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex around pagination">
|
||||
<el-pagination
|
||||
layout="sizes, prev, pager, next"
|
||||
:total="tableData ? tableData.length : 0"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
hide-on-single-page
|
||||
/>
|
||||
</div>
|
||||
<menu-edit
|
||||
:dialog="dialog"
|
||||
:data="dialogData"
|
||||
:run="update"
|
||||
:first-list="menus?.data.filter((e) => e.menu_id !== '000')"
|
||||
></menu-edit>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
255
web_vue/src/pages/sys-menu/menu-edit.vue
Normal file
@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import YlIconSelector from "@/components/yl-icon-selector.vue";
|
||||
import { iconSelector, userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { FormInstance } from "element-plus";
|
||||
// 编辑菜单信息页面
|
||||
const props = defineProps<{
|
||||
dialog: {
|
||||
title: string;
|
||||
visible: boolean;
|
||||
};
|
||||
data: sysMenuList;
|
||||
run: () => void;
|
||||
firstList?: sysMenuList[];
|
||||
}>();
|
||||
const formRef = ref<FormInstance>();
|
||||
const level = ref("first");
|
||||
const first = ref("");
|
||||
const second = ref("");
|
||||
const name = ref("");
|
||||
const route_only = ref(false);
|
||||
const path = ref("");
|
||||
const icon = ref("");
|
||||
const user_type = ref<string[]>([]);
|
||||
const white_list = ref<string[]>([]);
|
||||
const filterUserList = ref<typeUserList[]>([]);
|
||||
|
||||
const { data: sysSettings } = useRequest<{
|
||||
data: { name: string; data: sysSettings[] }[];
|
||||
}>(() => myRequest(`/admin/sys`, userinfo.token), {
|
||||
ready: () => Boolean(userinfo.token),
|
||||
});
|
||||
const { data: userList } = useRequest<{ data: typeUserList[] }>(
|
||||
() => myRequest(`/admin/user`, userinfo.token),
|
||||
{ ready: () => Boolean(userinfo.token) },
|
||||
);
|
||||
const { runAsync: updateMenu } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/admin/menus`,
|
||||
userinfo.token,
|
||||
props.data,
|
||||
props.dialog.title === "添加菜单" ? "post" : "put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
|
||||
const checkID = (_: any, value: string, callback: (e?: Error) => void) => {
|
||||
if (
|
||||
!value ||
|
||||
isNaN(Number(value)) ||
|
||||
(value.length !== 3 && level.value === "first") ||
|
||||
(value.length !== 5 && level.value === "second")
|
||||
) {
|
||||
callback(new Error("菜单id格式错误"));
|
||||
} else if (
|
||||
props.dialog.title === "新增菜单" &&
|
||||
props.firstList &&
|
||||
props.firstList.filter((e) => e.menu_id === value).length > 0
|
||||
) {
|
||||
callback(new Error("菜单id已存在"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const checkPath = (_: any, value: string, callback: (e?: Error) => void) => {
|
||||
if (level.value !== "first" && !value) {
|
||||
callback(new Error("二级菜单路径不能为空"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const rules = ref({
|
||||
menu_id: [{ validator: checkID, trigger: "blur" }],
|
||||
user_type: [{ required: true, message: "请选择用户权限", trigger: "blur" }],
|
||||
path: [
|
||||
{
|
||||
validator: checkPath,
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
name: [{ required: true, message: "请填写菜单名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
// emits("update", props.data);
|
||||
props.data.menu_id =
|
||||
level.value === "first" ? first.value : `${first.value}${second.value}`;
|
||||
props.data.user_type = user_type.value.join(",");
|
||||
props.data.white_list = white_list.value.join(",");
|
||||
props.data.name = name.value;
|
||||
props.data.path = path.value;
|
||||
props.data.icon = icon.value;
|
||||
props.data.route_only = route_only.value;
|
||||
if (!formRef.value) return;
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
updateMenu().then(() => props.run());
|
||||
}
|
||||
});
|
||||
};
|
||||
const userFilter = (val: string) => {
|
||||
filterUserList.value = userList.value
|
||||
? userList.value.data.filter(
|
||||
(item) =>
|
||||
item.username.indexOf(val) !== -1 ||
|
||||
item.nickname.indexOf(val) !== -1,
|
||||
)
|
||||
: [];
|
||||
};
|
||||
watch(props, () => {
|
||||
level.value = props.data.menu_id.length === 5 ? "second" : "first";
|
||||
first.value = props.data.menu_id.substring(0, 3);
|
||||
second.value = props.data.menu_id.substring(3, 5);
|
||||
name.value = props.data.name;
|
||||
path.value = props.data.path;
|
||||
icon.value = props.data.icon;
|
||||
route_only.value = props.data.route_only;
|
||||
user_type.value = props.data.user_type ? props.data.user_type.split(",") : [];
|
||||
white_list.value = props.data.white_list
|
||||
? props.data.white_list.split(",")
|
||||
: [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close>
|
||||
<el-form
|
||||
:model="data"
|
||||
label-width="100px"
|
||||
class="menu-edit"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
>
|
||||
<el-form-item label="菜单级别">
|
||||
<el-radio-group v-model="level">
|
||||
<el-radio value="first">一级菜单</el-radio>
|
||||
<el-radio value="second">二级菜单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="ID" prop="menu_id">
|
||||
<div class="flex between" style="width: 100%">
|
||||
<el-select
|
||||
:validate-event="false"
|
||||
v-model="first"
|
||||
v-if="level !== 'first'"
|
||||
clearable
|
||||
placeholder="选择一级菜单"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in firstList?.filter((e) => e.menu_id.length === 3)"
|
||||
:key="item.menu_id"
|
||||
:label="item.name"
|
||||
:value="item.menu_id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
:validate-event="false"
|
||||
v-model="second"
|
||||
v-if="level !== 'first'"
|
||||
placeholder="2位二级菜单ID,不可与已有ID重复"
|
||||
style="width: 50%"
|
||||
/>
|
||||
<el-input
|
||||
:validate-event="false"
|
||||
v-model="first"
|
||||
v-else
|
||||
placeholder="3位一级菜单ID,不可与已有ID重复"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input
|
||||
v-model="name"
|
||||
placeholder="填写菜单名称"
|
||||
:validate-event="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单路径" prop="path" :required="level !== 'first'">
|
||||
<el-input
|
||||
v-model="path"
|
||||
placeholder="填写菜单路径"
|
||||
:validate-event="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
content="点击选择图标"
|
||||
:validate-event="false"
|
||||
>
|
||||
<el-icon
|
||||
size="28px"
|
||||
class="menu-icon"
|
||||
@click="iconSelector.visible = true"
|
||||
><i :class="icon"></i></el-icon
|
||||
></el-tooltip>
|
||||
<yl-icon-selector @update="(value) => (icon = value)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="仅路由模式" prop="route_only">
|
||||
<el-switch :validate-event="false" v-model="route_only"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户权限" prop="user_type">
|
||||
<el-select
|
||||
v-model="user_type"
|
||||
placeholder="选择用户权限"
|
||||
multiple
|
||||
:validate-event="false"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in sysSettings?.data.filter(
|
||||
(e) => e.name === 'user_type',
|
||||
)[0].data"
|
||||
:key="item.value"
|
||||
:label="item.cn_name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="白名单" prop="white_list">
|
||||
<el-select
|
||||
v-model="white_list"
|
||||
placeholder="选择用户"
|
||||
multiple
|
||||
filterable
|
||||
:filter-method="userFilter"
|
||||
:validate-event="false"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filterUserList"
|
||||
:key="item.username"
|
||||
:label="item.nickname || item.username"
|
||||
:value="item.username"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit">确认</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.menu-edit {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
.menu-icon {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
127
web_vue/src/pages/sys-settings/yl-sys-settings.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
import { ref } from "vue";
|
||||
import _ from "lodash";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
// 空系统设置选项
|
||||
let _blank: sysSettings = {
|
||||
name: "",
|
||||
value: "",
|
||||
cn_name: "",
|
||||
d_type: "string",
|
||||
};
|
||||
// 新增或修改系统设置弹窗信息
|
||||
const modal = ref<{ visible: boolean; title: string; data: sysSettings }>({
|
||||
visible: false,
|
||||
title: "",
|
||||
data: _.cloneDeep(_blank),
|
||||
});
|
||||
//获取系统设置列表
|
||||
const { data, run, loading } = useRequest(() =>
|
||||
myRequest(`/admin/sys`, userinfo.token),
|
||||
);
|
||||
//更新系统设置信息
|
||||
const { runAsync: updateSys } = useRequest(
|
||||
() =>
|
||||
myRequest(
|
||||
`/admin/sys`,
|
||||
userinfo.token,
|
||||
modal.value.data,
|
||||
modal.value.title === "新增配置" ? "post" : "put",
|
||||
),
|
||||
{ manual: true },
|
||||
);
|
||||
// 弹窗保存方法
|
||||
const onSave = () => {
|
||||
updateSys()
|
||||
.then(() => {
|
||||
modal.value.visible = false;
|
||||
run();
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="
|
||||
modal = { visible: true, title: '新增配置', data: _.cloneDeep(_blank) }
|
||||
"
|
||||
>新增配置</el-button
|
||||
>
|
||||
</yl-action-div>
|
||||
<el-tabs tab-position="left" v-if="Boolean(data)">
|
||||
<el-tab-pane v-for="item in data.data" :label="item.name">
|
||||
<el-table :data="item.data" v-loading="loading">
|
||||
<el-table-column label="字段名" prop="name" align="center" />
|
||||
<el-table-column label="字段值" prop="value" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
type="info"
|
||||
v-for="item in scope.row.value ? scope.row.value.split(',') : []"
|
||||
v-if="scope.row.d_type === 'option'"
|
||||
>{{ item }}</el-tag
|
||||
>
|
||||
<span v-else>{{ scope.row.value }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中文名" prop="cn_name" align="center" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
@click="
|
||||
modal = {
|
||||
visible: true,
|
||||
title: '编辑配置',
|
||||
data: _.cloneDeep(scope.row),
|
||||
}
|
||||
"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog v-model="modal.visible" destroy-on-close :title="modal.title">
|
||||
<el-form label-width="80">
|
||||
<el-form-item label="字段类型">
|
||||
<el-radio-group v-model="modal.data.d_type">
|
||||
<el-radio value="string">字符串</el-radio>
|
||||
<el-radio value="option">选项</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="字段名" required>
|
||||
<el-input v-model="modal.data.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字段值" required>
|
||||
<el-input
|
||||
v-model="modal.data.value"
|
||||
v-if="modal.data.d_type == 'string'"
|
||||
/>
|
||||
<el-input-tag
|
||||
:model-value="modal.data.value ? modal.data.value.split(',') : []"
|
||||
@change="(v: string[]) => (modal.data.value = v.join(','))"
|
||||
v-else-if="modal.data.d_type == 'option'"
|
||||
/>
|
||||
<el-input v-model="modal.data.value" disabled v-else />
|
||||
</el-form-item>
|
||||
<el-form-item label="中文名">
|
||||
<el-input v-model="modal.data.cn_name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="onSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
116
web_vue/src/pages/sys-user/yl-sys-user.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import YlActionDiv from "@/components/yl-action-div.vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import YlUserinfo from "@/components/yl-layout/yl-userinfo.vue";
|
||||
import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatError } from "@/utils/utils.ts";
|
||||
import _ from "lodash";
|
||||
|
||||
const show = ref(false); // 修改用户信息弹窗状态
|
||||
const tmpUserinfo = ref<userinfo>({}); // 临时保存用户信息
|
||||
|
||||
// 获取用户列表
|
||||
const {
|
||||
data: userList,
|
||||
run: updateUserList,
|
||||
loading,
|
||||
} = useRequest(() => myRequest(`/admin/user`, userinfo.token));
|
||||
//更新用户信息
|
||||
const { runAsync: updateUserinfo } = useRequest(
|
||||
(data: userinfo) => myRequest(`/admin/user`, userinfo.token, data, "put"),
|
||||
{ manual: true },
|
||||
);
|
||||
//删除用户
|
||||
const { runAsync: deleteUserinfo } = useRequest(
|
||||
(data: string) =>
|
||||
myRequest(`/admin/user?username=${data}`, userinfo.token, {}, "delete"),
|
||||
{ manual: true },
|
||||
);
|
||||
//更新用户信息方法
|
||||
const toUpdateUserinfo = (data: userinfo) => {
|
||||
updateUserinfo(data)
|
||||
.then(() => {
|
||||
show.value = false;
|
||||
ElMessage.success("更新成功");
|
||||
updateUserList();
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
//删除用户信息方法
|
||||
const toDeleteUserinfo = (data: string) => {
|
||||
deleteUserinfo(data)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
updateUserList();
|
||||
})
|
||||
.catch((err) => ElMessage.error(formatError(err)));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<yl-action-div class-name="flex between">
|
||||
<el-button @click="updateUserList">刷新</el-button>
|
||||
</yl-action-div>
|
||||
<el-table :data="userList?.data" v-loading="loading">
|
||||
<el-table-column prop="avatar" label="头像" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="scope.row.avatar || '/static/image/avatar.png'"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="mobile"
|
||||
label="手机号"
|
||||
min-width="200"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" align="center" />
|
||||
<el-table-column prop="location" label="地址" width="200" align="center" />
|
||||
<el-table-column prop="type" label="用户类型" width="200" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
@click="
|
||||
() => {
|
||||
show = true;
|
||||
tmpUserinfo = _.cloneDeep(scope.row);
|
||||
}
|
||||
"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-popconfirm
|
||||
:title="`确认删除用户 ${scope.row.username} ?`"
|
||||
@confirm="() => toDeleteUserinfo(scope.row.username)"
|
||||
>
|
||||
<template #reference
|
||||
><el-button type="danger" size="small">删除</el-button></template
|
||||
>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog title="修改信息" v-model="show" width="40rem">
|
||||
<YlUserinfo @update="toUpdateUserinfo" :userinfo="tmpUserinfo" admin />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
53
web_vue/src/requests/request.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import axios from "axios";
|
||||
import { userinfo } from "@/store/store.ts";
|
||||
import { getCookie } from "@/utils/cookie.ts";
|
||||
|
||||
// 自定义请求方法
|
||||
export const myRequest = async (
|
||||
api: string,
|
||||
token: string = "",
|
||||
data: object = {},
|
||||
method: "get" | "post" | "put" | "delete" = "get",
|
||||
) => {
|
||||
// 未获取到token,返回登录
|
||||
if (!getCookie("token")) {
|
||||
userinfo.login = false;
|
||||
userinfo.token = "";
|
||||
}
|
||||
// 调用请求
|
||||
if (method === "post") {
|
||||
const res = await axios.post(api, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} else if (method === "get") {
|
||||
const res = await axios.get(api, {
|
||||
params: data,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} else if (method === "put") {
|
||||
const res = await axios.put(api, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} else if (method === "delete") {
|
||||
const res = await axios.delete(api, {
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} else return Promise.reject("暂不支持的请求");
|
||||
};
|
50
web_vue/src/router/router.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Home from "@/pages/home/index.vue";
|
||||
import YlSysMenu from "@/pages/sys-menu/index.vue";
|
||||
import YlAccount from "@/pages/account/index.vue";
|
||||
import YlAccountBill from "@/pages/account-bill/index.vue";
|
||||
import YlAccountBillDetail from "@/pages/account-bill/month.vue";
|
||||
import YlFile from "@/pages/files/index.vue";
|
||||
import ErrorPage from "@/pages/error/index.vue";
|
||||
import YlVideo from "@/pages/files/yl-video.vue";
|
||||
import YlSysInfo from "@/pages/sys-info/yl-sys-info.vue";
|
||||
import YlSysLog from "@/pages/sys-log/yl-sys-log.vue";
|
||||
import YlChess from "@/pages/games/yl-chess.vue";
|
||||
import YlChessRoom from "@/pages/games/yl-chess-room.vue";
|
||||
import YlBackgammon from "@/pages/games/yl-backgammon.vue";
|
||||
import YlBackgammonRoom from "@/pages/games/yl-backgammon-room.vue";
|
||||
import YlNote from "@/pages/notes/yl-note.vue";
|
||||
import YlNoteDetail from "@/pages/notes/yl-note-detail.vue";
|
||||
import YlSysSettings from "@/pages/sys-settings/yl-sys-settings.vue";
|
||||
import YlSysUser from "@/pages/sys-user/yl-sys-user.vue";
|
||||
|
||||
export const routes = [
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/:catchAll(.*)", component: ErrorPage },
|
||||
// { path: "/admin/user", component: Home },
|
||||
{ path: "/admin/menus", component: YlSysMenu },
|
||||
{ path: "/account", component: YlAccount },
|
||||
{ path: "/bill", component: YlAccountBill },
|
||||
{ path: "/bill/:date", component: YlAccountBillDetail },
|
||||
{ path: "/file", component: YlFile },
|
||||
{ path: "/file/video", component: YlVideo },
|
||||
{ path: "/sys-info", component: YlSysInfo },
|
||||
{ path: "/sys-log", component: YlSysLog },
|
||||
{ path: "/chess", component: YlChess },
|
||||
{ path: "/chess/:id", component: YlChessRoom },
|
||||
{ path: "/backgammon", component: YlBackgammon },
|
||||
{ path: "/backgammon/:id", component: YlBackgammonRoom },
|
||||
{ path: "/notes", component: YlNote },
|
||||
{ path: "/notes/:id", component: YlNoteDetail },
|
||||
{ path: "/sys-settings", component: YlSysSettings },
|
||||
{ path: "/sys-user", component: YlSysUser },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: routes.filter((route) =>
|
||||
["/", "/:catchAll(.*)"].includes(route.path),
|
||||
),
|
||||
});
|
||||
|
||||
export default router;
|
43
web_vue/src/store/store.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { reactive } from "vue";
|
||||
//登录页面
|
||||
export const loginData = reactive({
|
||||
login: true,
|
||||
username: "",
|
||||
password: "",
|
||||
repeatPass: "",
|
||||
autoLogin: false,
|
||||
});
|
||||
//用户登录信息
|
||||
export const userinfo: {
|
||||
login: boolean;
|
||||
username: string;
|
||||
token: string;
|
||||
type: string;
|
||||
userinfo: userinfo;
|
||||
} = reactive({
|
||||
login: false,
|
||||
username: "",
|
||||
token: "",
|
||||
type: "user",
|
||||
userinfo: {},
|
||||
});
|
||||
// websocket
|
||||
export const message = reactive({
|
||||
online: 0,
|
||||
});
|
||||
// 菜单
|
||||
export const menuData: {
|
||||
data: sysMenu[];
|
||||
default: string;
|
||||
} = reactive({
|
||||
data: [],
|
||||
default: "",
|
||||
});
|
||||
// 图标选择
|
||||
export const iconSelector = reactive({
|
||||
visible: false,
|
||||
iconList: [],
|
||||
selected: "",
|
||||
});
|
||||
//余额表及账单
|
||||
export const account = reactive({ show: false });
|
96
web_vue/src/type.d.ts
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
// store.ts-------------------------------------------------
|
||||
interface sysMenu {
|
||||
name: string;
|
||||
icon: string;
|
||||
path: string;
|
||||
menu_id: string;
|
||||
route_only: boolean;
|
||||
detail?: sysMenu[];
|
||||
}
|
||||
|
||||
interface userinfo {
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
location?: string;
|
||||
mobile?: string;
|
||||
nickname?: string;
|
||||
type?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
// sys-menu-------------------------------------------------
|
||||
interface sysMenuList {
|
||||
id?: number;
|
||||
menu_id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
route_only: boolean;
|
||||
user_type: string;
|
||||
white_list: string;
|
||||
children?: sysMenuList[];
|
||||
}
|
||||
interface sysSettings {
|
||||
name: string;
|
||||
cn_name: string;
|
||||
value: string;
|
||||
d_type: "string" | "option";
|
||||
}
|
||||
interface typeUserList {
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
mobile: string;
|
||||
email: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface accountBillData {
|
||||
date: string;
|
||||
balance: number;
|
||||
changes: number;
|
||||
duration: string;
|
||||
detail: accountBillDetailData[];
|
||||
}
|
||||
interface accountBillDetailData {
|
||||
card: string;
|
||||
balance: number;
|
||||
changes: number;
|
||||
}
|
||||
|
||||
// files-------------------------------------------------
|
||||
interface fileList {
|
||||
name: string;
|
||||
type: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
// sys-info-----------------------------------------------
|
||||
interface resSystem {
|
||||
data: { hostname: string; ip: string }[];
|
||||
}
|
||||
interface resSysInfo {
|
||||
data: {
|
||||
datetime: string;
|
||||
cpu_per: number;
|
||||
disk: { point: string; total: string; used: string; per: number }[];
|
||||
mem: { mem_total: string; mem_used: string; mem_per: number };
|
||||
net: { sent: string; rec: string };
|
||||
}[];
|
||||
}
|
||||
|
||||
// yl-chess-----------------------------------------------------
|
||||
interface chess {
|
||||
ID: number;
|
||||
players: string;
|
||||
current: string;
|
||||
is_end: string;
|
||||
status: string;
|
||||
winner: string;
|
||||
chess_id: string;
|
||||
}
|
||||
// yl-backgammon------------------------------------------------
|
||||
interface roomStatus {
|
||||
room_id: number;
|
||||
player: string;
|
||||
}
|
13
web_vue/src/utils/cookie.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// 获取页面cookie
|
||||
export const getCookie = (name: string) => {
|
||||
const strCookie = document.cookie;
|
||||
const arrCookie = strCookie.split("; ");
|
||||
let i: number;
|
||||
for (i = 0; i < arrCookie.length; i++) {
|
||||
const arr = arrCookie[i].split("=");
|
||||
if (arr[0] === name) {
|
||||
return arr.filter((_, index) => index !== 0).join("=");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
73
web_vue/src/utils/mebu.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { menuData, userinfo } from "@/store/store.ts";
|
||||
import router, { routes } from "@/router/router.ts";
|
||||
import _ from "lodash";
|
||||
|
||||
export const menuUpdate = () => {
|
||||
const currentPath = ref("/");
|
||||
const { data: menuList } = useRequest(
|
||||
() => myRequest(`/api/menu`, userinfo.token),
|
||||
{
|
||||
refreshDeps: [() => userinfo.token],
|
||||
ready: () => Boolean(userinfo.token),
|
||||
},
|
||||
);
|
||||
// 没有菜单信息,返回首页
|
||||
if (!menuList.value) {
|
||||
currentPath.value = window.location.href.split("#")[1];
|
||||
if (currentPath.value === "/") {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
// 获取菜单后更新路由
|
||||
watch(menuList, () => {
|
||||
router.clearRoutes();
|
||||
menuData.data = [];
|
||||
_.get(menuList, "value.data", [
|
||||
{ name: "首页", icon: "home", path: "/" },
|
||||
]).map((e: sysMenu) => {
|
||||
menuData.data.push({
|
||||
name: e.name,
|
||||
icon: e.icon,
|
||||
path: e.path,
|
||||
menu_id: e.menu_id,
|
||||
route_only: e.route_only,
|
||||
detail: e.detail,
|
||||
});
|
||||
// 菜单是最后登录的页面
|
||||
if (e.path === currentPath.value) {
|
||||
menuData.default = e.menu_id;
|
||||
}
|
||||
// 菜单存在二级菜单,添加二级菜单路由
|
||||
if (e.detail && e.detail.length > 0) {
|
||||
e.detail.map((m) => {
|
||||
if (routes.filter((item) => item.path === m.path).length > 0) {
|
||||
router.addRoute(routes.filter((item) => item.path === m.path)[0]);
|
||||
}
|
||||
if (m.path === currentPath.value) {
|
||||
menuData.default = m.menu_id;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (routes.filter((item) => item.path === e.path).length > 0) {
|
||||
router.addRoute(routes.filter((item) => item.path === e.path)[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
// router.addRoute(routes.filter((item) => item.path === "/234")[0]);
|
||||
// 路由添加全局匹配页面
|
||||
router.addRoute(routes.filter((item) => item.path === "/:catchAll(.*)")[0]);
|
||||
console.log(currentPath.value);
|
||||
if (menuList.value.length > 0) {
|
||||
router.push(currentPath.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
66
web_vue/src/utils/menu.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { useRequest } from "vue-request";
|
||||
import { myRequest } from "@/requests/request.ts";
|
||||
import { menuData, userinfo } from "@/store/store.ts";
|
||||
import { ref, watch } from "vue";
|
||||
import router, { routes } from "@/router/router.ts";
|
||||
import _ from "lodash";
|
||||
|
||||
export const updateMenu = () => {
|
||||
const currentPath = ref("/");
|
||||
const { data: menuList } = useRequest(
|
||||
() => myRequest(`/api/menu`, userinfo.token),
|
||||
{
|
||||
refreshDeps: [() => userinfo.token],
|
||||
ready: () => Boolean(userinfo.token),
|
||||
},
|
||||
);
|
||||
// 没有菜单信息,返回首页
|
||||
if (!menuList.value) {
|
||||
currentPath.value = window.location.href.split("#")[1];
|
||||
router.push("/");
|
||||
}
|
||||
// 获取菜单后更新路由
|
||||
watch(menuList, () => {
|
||||
router.clearRoutes();
|
||||
menuData.data = [];
|
||||
_.get(menuList, "value.data", [
|
||||
{ name: "首页", icon: "home", path: "/" },
|
||||
]).map((e: sysMenu) => {
|
||||
menuData.data.push({
|
||||
name: e.name,
|
||||
icon: e.icon,
|
||||
path: e.path,
|
||||
menu_id: e.menu_id,
|
||||
route_only: e.route_only,
|
||||
detail: e.detail,
|
||||
});
|
||||
// 菜单是最后登录的页面
|
||||
if (e.path === currentPath.value) {
|
||||
menuData.default = e.menu_id;
|
||||
}
|
||||
// 菜单存在二级菜单,添加二级菜单路由
|
||||
if (e.detail && e.detail.length > 0) {
|
||||
e.detail.map((m) => {
|
||||
if (routes.filter((item) => item.path === m.path).length > 0) {
|
||||
router.addRoute(routes.filter((item) => item.path === m.path)[0]);
|
||||
}
|
||||
if (m.path === currentPath.value) {
|
||||
menuData.default = m.menu_id;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (routes.filter((item) => item.path === e.path).length > 0) {
|
||||
router.addRoute(routes.filter((item) => item.path === e.path)[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
// router.addRoute(routes.filter((item) => item.path === "/234")[0]);
|
||||
// 路由添加全局匹配页面
|
||||
router.addRoute(routes.filter((item) => item.path === "/:catchAll(.*)")[0]);
|
||||
if (menuList.value.data.length > 0) {
|
||||
router.push(currentPath.value);
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
};
|
22
web_vue/src/utils/mouse.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
|
||||
// 按照惯例,组合式函数名以“use”开头
|
||||
export function useMouse() {
|
||||
// 被组合式函数封装和管理的状态
|
||||
const x = ref(0);
|
||||
const y = ref(0);
|
||||
|
||||
// 组合式函数可以随时更改其状态。
|
||||
function update(event: MouseEvent) {
|
||||
x.value = event.pageX;
|
||||
y.value = event.pageY;
|
||||
}
|
||||
|
||||
// 一个组合式函数也可以挂靠在所属组件的生命周期上
|
||||
// 来启动和卸载副作用
|
||||
onMounted(() => window.addEventListener("mousemove", update));
|
||||
onUnmounted(() => window.removeEventListener("mousemove", update));
|
||||
|
||||
// 通过返回值暴露所管理的状态
|
||||
return { x, y };
|
||||
}
|
58
web_vue/src/utils/utils.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import jsEncrypt from "jsencrypt";
|
||||
import _ from "lodash";
|
||||
// rsa非对称加密
|
||||
export const rsaEncrypt = (pubKey: string, data: string) => {
|
||||
const encrypt = new jsEncrypt();
|
||||
encrypt.setPublicKey(pubKey);
|
||||
return encrypt.encrypt(data);
|
||||
};
|
||||
// 格式化接口返回的错误信息
|
||||
export const formatError = (res: any) => _.get(res, "response.data.msg", "");
|
||||
|
||||
//数字单位转换,s:带单位字符串,数字和单位以空格分割;d:计划转换的单位,默认MB;t:测试使用,打印s
|
||||
export const transNum = (s: string, d?: string, t?: string) => {
|
||||
if (t) {
|
||||
console.log(s);
|
||||
}
|
||||
// 未传参返回0
|
||||
if (!s) {
|
||||
return 0;
|
||||
}
|
||||
let n = Number(s.split(" ")[0]);
|
||||
const list = ["B", "KB", "MB", "GB", "TB"];
|
||||
// 数字单位由d转换为MB
|
||||
if (d && d !== "MB") {
|
||||
let a = list.indexOf(d);
|
||||
if (a < 2) {
|
||||
while (2 - a) {
|
||||
n *= 1024;
|
||||
a += 1;
|
||||
}
|
||||
} else if (a > 2) {
|
||||
while (a - 2) {
|
||||
n /= 1024;
|
||||
a -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 单位由s转换为d
|
||||
const b = s.split(" ")[1];
|
||||
if (b) {
|
||||
if (b.indexOf("KB") !== -1) {
|
||||
return Number((n / 1024).toFixed(3));
|
||||
} else if (b.indexOf("MB") !== -1) {
|
||||
return Number(n.toFixed(3));
|
||||
} else if (b.indexOf("GB") !== -1) {
|
||||
return Number((n * 1024).toFixed(3));
|
||||
} else if (b.indexOf("TB") !== -1) {
|
||||
return Number((n * 1024 * 1024).toFixed(3));
|
||||
} else return Number((n / 1024 / 1024).toFixed(3));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
// 获取设备类型
|
||||
export const isMobile = () => {
|
||||
return !!window.navigator.userAgent.match(
|
||||
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i,
|
||||
);
|
||||
};
|
13
web_vue/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module "lodash";
|
||||
declare module "@kangc/v-md-editor";
|
||||
declare module "@kangc/v-md-editor/lib/theme/vuepress.js";
|
||||
declare module "@kangc/v-md-editor/lib/plugins/emoji/index";
|
||||
declare module "prismjs";
|
||||
declare module "prismjs";
|
30
web_vue/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@": ["src"],
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
11
web_vue/tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
38
web_vue/vite.config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { defineConfig, splitVendorChunkPlugin } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { resolve } from "path";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), splitVendorChunkPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"/api_django": {
|
||||
target: "http://localhost:8000/api/",
|
||||
rewrite: (path) => path.replace(/^\/api_django/, ""),
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/api_file": {
|
||||
target: "http://localhost:8081",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/admin": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/static": {
|
||||
target: "https://git-ylsa0.cn",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|