This commit is contained in:
xtaodada 2023-02-01 21:45:41 +08:00
parent 83e329309a
commit 4141848cfb
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
58 changed files with 36561 additions and 99 deletions

4
.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{json,js,jsx,ts,tsx,vue,scss,yml,yaml}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

24
.eslintrc.js Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
indent: ['error', 4, { SwitchCase: 1 }],
'@typescript-eslint/no-explicit-any': ['off'],
'generator-star-spacing': 'off',
'no-throw-literal': 'off',
'no-return-assign': 'off',
'no-return-await': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

120
.gitignore vendored
View File

@ -1,104 +1,26 @@
# Logs
logs
*.log
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
auto-imports.d.ts
components.d.ts

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry="https://registry.npm.taobao.org/"

View File

@ -1,2 +1,7 @@
# Web-Console
PagerMaid-Pyro Web-Console
# Thanks
[Amiya-Bot-console2](https://github.com/AmiyaBot/Amiya-Bot-console2)

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

3
lint-staged.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
'*.{js,jsx,vue,ts,tsx}': 'vue-cli-service lint'
}

25162
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "PagerMaid-Pyro-Web-Console",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"serverTest": "node serverTest.js",
"fix": "eslint --fix --ext .js,.ts,.vue src"
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.9",
"axios": "^0.27.2",
"core-js": "^3.8.3",
"echarts": "^5.4.0",
"element-plus": "^2.2.13",
"jquery": "^3.6.1",
"marked": "^4.1.0",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@types/jquery": "^3.5.14",
"@types/marked": "^4.0.7",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0",
"@vue/eslint-config-typescript": "^9.1.0",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^8.0.3",
"express": "^4.18.1",
"lint-staged": "^11.1.2",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"typescript": "~4.5.5",
"unplugin-auto-import": "^0.11.2",
"unplugin-vue-components": "^0.22.4",
"webpack": "^5.74.0"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"license": "AGPL3"
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

0
public/notice.txt Normal file
View File

6
serverTest.js Normal file
View File

@ -0,0 +1,6 @@
const express = require('express')
const app = express()
app.use(express.static('dist')).listen(8080)
console.log('server open: http://localhost:8080')

76
src/App.vue Normal file
View File

@ -0,0 +1,76 @@
<template>
<div style="height: calc(100% - 20px)">
<router-view/>
</div>
<div class="cc">
Copyright © 2023 &nbsp;&nbsp;<a href="https://github.com/TeamPGM/PagerMaid-Pyro" target="_blank" class="link-secondary"> PagerMaid-Pyro
</a>&nbsp;&nbsp; X &nbsp;&nbsp;<a target="_blank" href="https://github.com/AmiyaBot/Amiya-Bot-console2" class="link-secondary" rel="noopener">
Amiya-Bot-console2
</a>
</div>
</template>
<script lang="ts">
import { Vue } from 'vue-class-component'
export default class App extends Vue {
}
</script>
<style scoped lang="scss">
.cc {
height: 20px;
display: flex;
align-items: center;
justify-content: center;
a {
font-size: 12px;
color: var(--el-color-info);
}
}
</style>
<style lang="scss">
* {
box-sizing: border-box;
font-family: Helvetica, Arial, sans-serif !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-overflow: initial !important;
&:not(svg) {
font-size: 14px;
}
}
html,
body {
margin: 0;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
z-index: 0;
}
:root {
scroll-behavior: smooth;
--c-main: var(--el-color-primary);
--c-main-light: var(--el-color-primary-light-8);
}
.el-notification__content {
word-break: break-all;
word-wrap: break-word;
text-align: inherit;
}
.v-loading {
background-color: transparent !important;
backdrop-filter: blur(10px);
}
</style>

View File

@ -0,0 +1,940 @@
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: #24292f;
background-color: #ffffff;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: ' ';
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body [hidden] {
display: none !important;
}
.markdown-body a {
background-color: transparent;
color: #0969da;
text-decoration: none;
}
.markdown-body a:active,
.markdown-body a:hover {
outline-width: 0;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.markdown-body b,
.markdown-body strong {
font-weight: 600;
}
.markdown-body dfn {
font-style: italic;
}
.markdown-body h1 {
margin: .67em 0;
font-weight: 600;
padding-bottom: .3em;
font-size: 2em;
border-bottom: 1px solid hsla(210,18%,87%,1);
}
.markdown-body mark {
background-color: #fff8c5;
color: #24292f;
}
.markdown-body small {
font-size: 90%;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: #ffffff;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace,monospace;
font-size: 1em;
}
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid hsla(210,18%,87%,1);
height: .25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.markdown-body input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body [type=button],
.markdown-body [type=reset],
.markdown-body [type=submit] {
-webkit-appearance: button;
}
.markdown-body [type=button]::-moz-focus-inner,
.markdown-body [type=reset]::-moz-focus-inner,
.markdown-body [type=submit]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.markdown-body [type=button]:-moz-focusring,
.markdown-body [type=reset]:-moz-focusring,
.markdown-body [type=submit]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.markdown-body [type=checkbox],
.markdown-body [type=radio] {
box-sizing: border-box;
padding: 0;
}
.markdown-body [type=number]::-webkit-inner-spin-button,
.markdown-body [type=number]::-webkit-outer-spin-button {
height: auto;
}
.markdown-body [type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.markdown-body [type=search]::-webkit-search-cancel-button,
.markdown-body [type=search]::-webkit-search-decoration {
-webkit-appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
color: inherit;
opacity: .54;
}
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body details:not([open])>*:not(summary) {
display: none !important;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
line-height: 10px;
color: #24292f;
vertical-align: middle;
background-color: #f6f8fa;
border: solid 1px rgba(175,184,193,0.2);
border-bottom-color: rgba(175,184,193,0.2);
border-radius: 6px;
box-shadow: inset 0 -1px 0 rgba(175,184,193,0.2);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h2 {
font-weight: 600;
padding-bottom: .3em;
font-size: 1.5em;
border-bottom: 1px solid hsla(210,18%,87%,1);
}
.markdown-body h3 {
font-weight: 600;
font-size: 1.25em;
}
.markdown-body h4 {
font-weight: 600;
font-size: 1em;
}
.markdown-body h5 {
font-weight: 600;
font-size: .875em;
}
.markdown-body h6 {
font-weight: 600;
font-size: .85em;
color: #57606a;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 1em;
color: #57606a;
border-left: .25em solid #d0d7de;
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body tt,
.markdown-body code {
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
word-wrap: normal;
}
.markdown-body .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.markdown-body ::placeholder {
color: #6e7781;
opacity: 1;
}
.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body .pl-c {
color: #6e7781;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #0550ae;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #8250df;
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: #24292f;
}
.markdown-body .pl-ent {
color: #116329;
}
.markdown-body .pl-k {
color: #cf222e;
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: #0a3069;
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: #953800;
}
.markdown-body .pl-bu {
color: #82071e;
}
.markdown-body .pl-ii {
color: #f6f8fa;
background-color: #82071e;
}
.markdown-body .pl-c2 {
color: #f6f8fa;
background-color: #cf222e;
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: #116329;
}
.markdown-body .pl-ml {
color: #3b2300;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: #0550ae;
}
.markdown-body .pl-mi {
font-style: italic;
color: #24292f;
}
.markdown-body .pl-mb {
font-weight: bold;
color: #24292f;
}
.markdown-body .pl-md {
color: #82071e;
background-color: #FFEBE9;
}
.markdown-body .pl-mi1 {
color: #116329;
background-color: #dafbe1;
}
.markdown-body .pl-mc {
color: #953800;
background-color: #ffd8b5;
}
.markdown-body .pl-mi2 {
color: #eaeef2;
background-color: #0550ae;
}
.markdown-body .pl-mdr {
font-weight: bold;
color: #8250df;
}
.markdown-body .pl-ba {
color: #57606a;
}
.markdown-body .pl-sg {
color: #8c959f;
}
.markdown-body .pl-corl {
text-decoration: underline;
color: #0a3069;
}
.markdown-body [data-catalyst] {
display: block;
}
.markdown-body g-emoji {
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
line-height: 1;
vertical-align: -0.075em;
}
.markdown-body g-emoji img {
width: 1em;
height: 1em;
}
.markdown-body::before {
display: table;
content: "";
}
.markdown-body::after {
display: table;
clear: both;
content: "";
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .absent {
color: #cf222e;
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body sup>a::before {
content: "[";
}
.markdown-body sup>a::after {
content: "]";
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #24292f;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
padding: 0 .2em;
font-size: inherit;
}
.markdown-body ul.no-list,
.markdown-body ol.no-list {
padding: 0;
list-style-type: none;
}
.markdown-body ol[type="1"] {
list-style-type: decimal;
}
.markdown-body ol[type=a] {
list-style-type: lower-alpha;
}
.markdown-body ol[type=i] {
list-style-type: lower-roman;
}
.markdown-body div>ol:not([type]) {
list-style-type: decimal;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body li+li {
margin-top: .25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table th {
font-weight: 600;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.markdown-body table tr {
background-color: #ffffff;
border-top: 1px solid hsla(210,18%,87%,1);
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body table img {
background-color: transparent;
}
.markdown-body img[align=right] {
padding-left: 20px;
}
.markdown-body img[align=left] {
padding-right: 20px;
}
.markdown-body .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.markdown-body span.frame {
display: block;
overflow: hidden;
}
.markdown-body span.frame>span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #d0d7de;
}
.markdown-body span.frame span img {
display: block;
float: left;
}
.markdown-body span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #24292f;
}
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-center>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center;
}
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-right>span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.markdown-body span.align-right span img {
margin: 0;
text-align: right;
}
.markdown-body span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.markdown-body span.float-left span {
margin: 13px 0 0;
}
.markdown-body span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.markdown-body span.float-right>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.markdown-body code,
.markdown-body tt {
padding: .2em .4em;
margin: 0;
font-size: 85%;
background-color: rgba(175,184,193,0.2);
border-radius: 6px;
}
.markdown-body code br,
.markdown-body tt br {
display: none;
}
.markdown-body del code {
text-decoration: inherit;
}
.markdown-body pre code {
font-size: 100%;
}
.markdown-body pre>code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .csv-data td,
.markdown-body .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.markdown-body .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: #ffffff;
border: 0;
}
.markdown-body .csv-data tr {
border-top: 0;
}
.markdown-body .csv-data th {
font-weight: 600;
background: #f6f8fa;
border-top: 0;
}
.markdown-body .footnotes {
font-size: 12px;
color: #57606a;
border-top: 1px solid #d0d7de;
}
.markdown-body .footnotes ol {
padding-left: 16px;
}
.markdown-body .footnotes li {
position: relative;
}
.markdown-body .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid #0969da;
border-radius: 6px;
}
.markdown-body .footnotes li:target {
color: #24292f;
}
.markdown-body .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item label {
font-weight: 400;
}
.markdown-body .task-list-item.enabled label {
cursor: pointer;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item .handle {
display: none;
}
.markdown-body .task-list-item-checkbox {
margin: 0 .2em .25em -1.6em;
vertical-align: middle;
}
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em .25em .2em;
}
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}

1
src/assets/icon/book.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661495228242" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M168.106667 621.44l120.746666 57.962667 223.274667 108.138666 215.317333-104.32 128.768-61.674666a64 64 0 0 1-29.952 84.970666l-286.229333 138.624a64 64 0 0 1-55.808 0L197.994667 706.517333A64 64 0 0 1 168.106667 621.44z m687.829333-133.930667a64 64 0 0 1-29.674667 85.546667L540.010667 711.68a64 64 0 0 1-55.808 0L197.994667 573.056A64 64 0 0 1 166.826667 490.88l317.013333 149.525333 28.288 13.696 286.229333-138.624-0.149333-0.064 57.728-27.882666zM540.032 185.792l286.208 138.602667a64 64 0 0 1 0 115.2l-286.208 138.624a64 64 0 0 1-55.808 0L197.994667 439.594667a64 64 0 0 1 0-115.2L484.224 185.813333a64 64 0 0 1 55.808 0z" p-id="11718" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661495006634" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3251" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 42.666667A464.64 464.64 0 0 0 42.666667 502.186667 460.373333 460.373333 0 0 0 363.52 938.666667c23.466667 4.266667 32-9.813333 32-22.186667v-78.08c-130.56 27.733333-158.293333-61.44-158.293333-61.44a122.026667 122.026667 0 0 0-52.053334-67.413333c-42.666667-28.16 3.413333-27.733333 3.413334-27.733334a98.56 98.56 0 0 1 71.68 47.36 101.12 101.12 0 0 0 136.533333 37.973334 99.413333 99.413333 0 0 1 29.866667-61.44c-104.106667-11.52-213.333333-50.773333-213.333334-226.986667a177.066667 177.066667 0 0 1 47.36-124.16 161.28 161.28 0 0 1 4.693334-121.173333s39.68-12.373333 128 46.933333a455.68 455.68 0 0 1 234.666666 0c89.6-59.306667 128-46.933333 128-46.933333a161.28 161.28 0 0 1 4.693334 121.173333A177.066667 177.066667 0 0 1 810.666667 477.866667c0 176.64-110.08 215.466667-213.333334 226.986666a106.666667 106.666667 0 0 1 32 85.333334v125.866666c0 14.933333 8.533333 26.88 32 22.186667A460.8 460.8 0 0 0 981.333333 502.186667 464.64 464.64 0 0 0 512 42.666667" fill="#ffffff" p-id="3252"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661495994846" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11919" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M556.586667 159.36l288.490666 183.914667A64 64 0 0 1 874.666667 397.248v392.746667a64 64 0 0 1-64 64H555.456l0.021333-196.992H490.666667v196.992H234.666667a64 64 0 0 1-64-64v-398.293334a64 64 0 0 1 30.272-54.4l287.530666-178.346666a64 64 0 0 1 68.138667 0.426666zM810.666667 790.016V397.226667L522.197333 213.333333 234.666667 391.68v398.336h192v-197.013333h192.810666v196.992H810.666667z" p-id="11920" fill="#606cff"></path></svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661496049213" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17401" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M644.8 581.568l160.64 187.456a64 64 0 0 1-48.597 105.643H267.157a64 64 0 0 1-48.597-105.643l160.661-187.435a253.813 253.813 0 0 0 61.206 26.944l-173.27 202.134h489.686l-173.27-202.134a254.613 254.613 0 0 0 61.227-26.965zM512 149.333c117.824 0 213.333 95.51 213.333 213.334S629.824 576 512 576s-213.333-95.51-213.333-213.333S394.176 149.333 512 149.333z m0 64A149.333 149.333 0 1 0 512 512a149.333 149.333 0 0 0 0-298.667z" p-id="17402" fill="#606cff"></path></svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1674974462982" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6752" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M384 768l320 0 0-64L384 704 384 768zM384 512l320 0L704 448 384 448 384 512zM832 128 256 128C185.6 128 128 185.6 128 256l0 576c0 70.4 57.6 128 128 128l576 0c70.4 0 128-57.6 128-128L960 256C960 185.6 902.4 128 832 128zM768 544C768 561.92 753.92 576 736 576L384 576l0 64 352 0c17.92 0 32 14.08 32 32l0 128c0 17.92-14.08 32-32 32l-384 0C334.08 832 320 817.92 320 800l0-384C320 398.08 334.08 384 352 384l384 0C753.92 384 768 398.08 768 416L768 544zM832 352C832 369.92 817.92 384 800 384 782.08 384 768 369.92 768 352L768 320 320 320l0 32C320 369.92 305.92 384 288 384S256 369.92 256 352l0-64C256 270.08 270.08 256 288 256L512 256 512 224C512 206.08 526.08 192 544 192 561.92 192 576 206.08 576 224L576 256l224 0C817.92 256 832 270.08 832 288L832 352z" p-id="6753" fill="#67C23A"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1663732259269" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2252" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M420.693333 896a46.506667 46.506667 0 0 1-11.093333 0 42.666667 42.666667 0 0 1-30.293333-52.48l183.04-682.666667a42.666667 42.666667 0 1 1 82.346666 22.186667l-183.04 682.666667a42.666667 42.666667 0 0 1-40.96 30.293333zM256 725.333333a42.666667 42.666667 0 0 1-30.293333-12.373333L85.333333 572.16a85.333333 85.333333 0 0 1 0-120.32l140.373334-140.8a42.666667 42.666667 0 1 1 60.586666 60.586667L145.493333 512l140.8 140.373333a42.666667 42.666667 0 0 1 0 60.586667A42.666667 42.666667 0 0 1 256 725.333333zM768 725.333333a42.666667 42.666667 0 0 1-30.293333-12.373333 42.666667 42.666667 0 0 1 0-60.586667l140.8-140.373333-140.8-140.373333a42.666667 42.666667 0 1 1 60.586666-60.586667l140.373334 140.8a85.333333 85.333333 0 0 1 0 120.32l-140.373334 140.8A42.666667 42.666667 0 0 1 768 725.333333z" p-id="2253" fill="#409eff"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661488282662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6230" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M405.333333 149.333333l67.562667 184.234667h91.776L632.234667 149.333333h64l-67.562667 184.234667h110.229333a64 64 0 0 1 64 64v407.274667a64 64 0 0 1-64 64H285.098667a64 64 0 0 1-64-64v-407.253334a64 64 0 0 1 64-64l123.797333-0.021333L341.333333 149.333333h64z m333.568 248.234667H285.098667v407.274667h453.802666v-407.253334zM192 496.490667v213.333333H128v-213.333333h64z m698.176 0v213.333333h-64v-213.333333h64zM405.333333 519.744a42.666667 42.666667 0 1 1 0 85.333333 42.666667 42.666667 0 0 1 0-85.333333z m213.333334 0a42.666667 42.666667 0 1 1 0 85.333333 42.666667 42.666667 0 0 1 0-85.333333z" p-id="6231" fill="#409EFF"></path></svg>

After

Width:  |  Height:  |  Size: 977 B

1
src/assets/icon/set.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1661488200226" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5921" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M448.362667 166.826667l113.6 0.170666a64 64 0 0 1 63.893333 63.914667l0.042667 18.517333a301.461333 301.461333 0 0 1 62.101333 34.88l15.210667-8.746666a64 64 0 0 1 87.296 23.381333l56.938666 98.304a64 64 0 0 1-19.989333 85.397333l-3.477333 2.133334-15.274667 8.810666c2.624 24.234667 2.304 48.853333-1.130667 73.322667l10.794667 6.250667a64 64 0 0 1 25.216 84.117333l-1.770667 3.306667-53.333333 92.373333a64 64 0 0 1-84.117333 25.216l-3.328-1.792-14.741334-8.533333a298.538667 298.538667 0 0 1-59.626666 33.28v25.386666a64 64 0 0 1-59.989334 63.957334l-4.074666 0.128-113.6-0.170667a64 64 0 0 1-63.893334-63.893333l-0.064-30.613334a302.613333 302.613333 0 0 1-50.069333-29.696l-27.221333 15.658667a64 64 0 0 1-87.296-23.402667l-56.938667-98.282666a64 64 0 0 1 19.989333-85.418667l3.477334-2.133333 27.690666-15.936c-2.133333-20.266667-2.24-40.768-0.192-61.226667l-30.741333-17.770667A64 64 0 0 1 158.506667 393.6l1.792-3.306667 53.333333-92.373333a64 64 0 0 1 84.117333-25.216l3.306667 1.792 26.794667 15.466667a297.984 297.984 0 0 1 56.426666-34.666667v-24.362667a64 64 0 0 1 59.989334-63.978666l4.074666-0.128z m-0.085334 64l0.064 65.066666-36.778666 17.301334c-15.744 7.402667-30.613333 16.533333-44.309334 27.221333l-34.005333 26.538667-62.570667-36.138667-1.6-0.896-53.333333 92.373333 66.56 38.421334-4.138667 41.152c-1.6 15.978667-1.536 32.106667 0.149334 48.085333l4.394666 41.429333-63.786666 36.736 56.917333 98.282667 63.338667-36.416 33.6 24.597333a237.994667 237.994667 0 0 0 39.466666 23.424l36.736 17.258667 0.128 71.168 113.578667 0.170667-0.064-68.16 39.466667-16.426667a234.538667 234.538667 0 0 0 46.826666-26.112l33.578667-24.128 50.56 29.184 53.290667-92.394667-48.128-27.818666 5.973333-42.688c2.666667-19.093333 2.965333-38.421333 0.896-57.6l-4.48-41.450667 51.456-29.696-56.938667-98.282667-51.2 29.504-33.621333-24.512a238.037333 238.037333 0 0 0-48.938667-27.498666l-39.381333-16.341334-0.128-61.184-113.578667-0.170666z m127.381334 183.722666a128.170667 128.170667 0 0 1 46.890666 174.933334 127.829333 127.829333 0 0 1-174.762666 46.848 128.170667 128.170667 0 0 1-46.869334-174.933334 127.829333 127.829333 0 0 1 174.741334-46.848z m-119.317334 78.805334a64.170667 64.170667 0 0 0 23.466667 87.573333 63.829333 63.829333 0 0 0 87.296-23.402667 64.170667 64.170667 0 0 0-20.266667-85.589333l-3.2-1.984-3.306666-1.770667a63.829333 63.829333 0 0 0-83.989334 25.173334z" p-id="5922" fill="#409EFF"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,70 @@
export interface WidthList {
[key: string]: number
}
export function calcMinWidth (table: HTMLTableElement, cellMinWidth = 50): WidthList {
const maxWidth = table.clientWidth
const widthList: WidthList = {}
let total = 0
if (maxWidth === 0) {
return {}
}
const tr = table.querySelectorAll('tr')
tr.forEach(td => {
td.querySelectorAll('td, th').forEach((item) => {
const cell = item.querySelector<HTMLElement>('.cell')
const cellItems = item.querySelectorAll<HTMLElement>('.cell > *')
const name: RegExpMatchArray | null = item.className.match(/mark_(\S+)/)
let width = cellMinWidth
for (const el of cellItems) {
width += el.offsetWidth
}
if (name) {
const n = name[1]
if (n === 'operation') {
width += 30
}
if (n in widthList) {
if (widthList[n] < width) {
widthList[n] = width
}
} else {
widthList[n] = cell?.offsetWidth || 0
}
}
})
})
// 计算总宽度
for (const i in widthList) {
total += widthList[i]
}
if (total < maxWidth) {
let length = Object.keys(widthList).length
if ('selection' in widthList) {
length -= 1
}
if ('operation' in widthList) {
length -= 1
}
const empty = (maxWidth - total) / length
for (const i in widthList) {
if (i === 'selection' || i === 'operation') {
continue
}
widthList[i] += empty
}
}
return widthList
}

View File

@ -0,0 +1,45 @@
.v-table {
.header-area {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
.v-table-header {
font-weight: 100;
color: var(--c-main);
}
.v-bottom {
margin-top: 10px;
display: flex;
align-items: center;
.v-bottom-icon {
color: var(--el-text-color-regular);
cursor: pointer;
}
.v-bottom-icon:hover {
color: var(--c-main);
}
}
.cell {
width: auto;
white-space: nowrap !important;
display: flex;
}
.cell > .el-link:not(:last-child) {
margin-right: 10px;
}
.el-pagination__sizes {
margin-left: 15px;
}
.el-pagination__jump {
margin-left: 0;
}
}

View File

@ -0,0 +1,202 @@
<template>
<div class="v-table">
<div class="header-area">
<div>
<slot name="header"></slot>
</div>
<el-input style="width: 520px" v-model="searchInput" placeholder="输入任意值搜索..."
@change="executeLoad(true)">
<template #append>
<el-button :icon="searchIcon"/>
</template>
</el-input>
</div>
<el-table :data="tableData" stripe header-cell-class-name="v-table-header" ref="table" empty-text="暂无数据"
:max-height="600">
<el-table-column v-for="(item, index) in columns" show-overflow-tooltip
:width="item.width || colWidth[item.field]"
:class-name="'mark_' + item.field"
:key="index"
:prop="item.field"
:label="item.title">
<template #default="scope">
<slot name="cell"
:row="scope.row"
:field="item.field"
:value="scope.row[item.field]"
:index="scope.$index">
<div v-if="item.format"
v-html="item.format(scope.row, scope.row[item.field], scope.$index)"></div>
<div v-else v-html="scope.row[item.field]"></div>
</slot>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" class-name="mark_operation" :width="colWidth.operation"
v-if="this.$slots.operations">
<template #default="scope">
<slot name="operations" :row="scope.row" :index="scope.$index"></slot>
</template>
</el-table-column>
</el-table>
<div class="v-bottom">
<el-pagination :layout="pagination ? 'total, prev, pager, next, sizes, jumper' : 'total'" :small="true"
:background="true"
v-model:currentPage="currentPage"
v-model:page-size="pageSize"
v-model:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"/>
<el-icon class="v-bottom-icon" :size="20" @click="executeLoad()">
<Refresh/>
</el-icon>
</div>
</div>
</template>
<script lang="ts">
import { shallowRef } from 'vue'
import { Options, Vue } from 'vue-class-component'
import { calcMinWidth, WidthList } from '@/components/table/calcMinWidth'
import { Refresh, Search } from '@element-plus/icons-vue'
import { DictArray, StringDict } from '@/lib/common'
interface ColumnField {
title: string
field?: string
width?: number
format?: (row: StringDict, value: any, index: number) => string
}
interface ColumnConfig {
[field: string]: ColumnField
}
interface QueryData extends StringDict {
currentPage: number
pageSize: number
search?: string
}
interface PaginationData {
total: number
list: DictArray
}
type QueryMethod = (data: QueryData) => void
export {
QueryData
}
@Options({
components: {
Refresh
},
props: {
load: {
type: Function,
default: () => null
}
},
computed: {
table () {
return this.$refs.table
}
},
methods: {
resetWidth (): void {
setTimeout(() => {
this.colWidth = calcMinWidth(this.table.$el)
}, 200)
}
},
watch: {
tableData: {
handler () {
this.resetWidth()
},
deep: true
}
}
})
export default class VTable extends Vue {
load!: QueryMethod
private searchIcon = shallowRef(Search)
private searchInput = ''
private tableData: DictArray = []
private columns: Array<ColumnField> = []
private colWidth: WidthList = {}
private pagination = false
private currentPage = 1
private pageSize = 10
private total = 0
public getColumns () {
return {
columns: this.columns,
colWidth: this.colWidth
}
}
public setData (data: DictArray): void {
this.tableData = data
this.total = data.length
this.pagination = false
}
public setPaginationData (data: PaginationData): void {
this.tableData = data.list
this.total = data.total
this.pagination = true
}
public setColumns (config: ColumnConfig): void {
const columns: Array<ColumnField> = []
for (const field in config) {
const item = config[field]
if (item.constructor === String) {
columns.push({
field: field,
title: item
})
} else {
if (!item.field) {
item.field = field
}
columns.push(item)
}
}
this.columns = columns
this.executeLoad()
}
public executeLoad (reset = false) {
if (reset) {
this.currentPage = 1
}
this.load({
currentPage: this.currentPage,
pageSize: this.pageSize,
search: this.searchInput
})
}
public handleSizeChange () {
this.executeLoad()
}
public handleCurrentChange () {
this.executeLoad()
}
}
</script>
<style lang="scss">
</style>

View File

@ -0,0 +1,41 @@
<template>
<el-dialog v-model="dialogVisible" :title="title" :append-to-body="appendToBody" :width="width">
<template #default>
<slot></slot>
</template>
</el-dialog>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
@Options({
props: {
title: String,
width: String,
appendToBody: Boolean,
labelWidth: {
type: String,
default: () => '100px'
}
}
})
export default class VDialog extends Vue {
title!: string
labelWidth!: string
private dialogVisible = false
public show () {
this.dialogVisible = true
}
public hide () {
this.dialogVisible = false
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,49 @@
<template>
<el-dialog v-model="dialogVisible" :title="title" :append-to-body="appendToBody" :width="width">
<el-form :model="form" :label-width="labelWidth">
<template #default>
<slot></slot>
</template>
</el-form>
<template #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { StringDict } from '@/lib/common'
@Options({
props: {
form: Object,
title: String,
width: Number,
appendToBody: Boolean,
labelWidth: {
type: String,
default: () => '100px'
}
}
})
export default class VFormDialog extends Vue {
form!: StringDict
title!: string
labelWidth!: string
private dialogVisible = false
public show () {
this.dialogVisible = true
}
public hide () {
this.dialogVisible = false
}
}
</script>
<style scoped lang="scss">
</style>

83
src/lib/common.ts Normal file
View File

@ -0,0 +1,83 @@
interface StringDict {
[key: string]: any
}
type DictArray = Array<StringDict>
export {
StringDict,
DictArray
}
export default class Common {
static setData (name: string, data: any) {
let dataStr = null
if (typeof data === 'object') {
dataStr = JSON.stringify(data)
}
if (typeof data === 'string' || typeof data === 'number') {
dataStr = data.toString()
}
if (dataStr) {
localStorage.setItem('pgp-' + name, dataStr)
}
}
static getData (name: string) {
let data = localStorage.getItem('pgp-' + name)
if (data) {
try {
data = JSON.parse(data)
} catch (e) {
}
return data
}
}
static removeData (name: string) {
localStorage.removeItem('pgp-' + name)
}
static correctDate (value: Date | number | string, onInvalid?: () => Date): Date {
if (value.constructor === Date) {
return value
}
if (value.constructor === Number && value.toString().length < 13) {
value = value * 1000
}
const result = new Date(value)
if (result.toString() === 'Invalid Date') {
if (onInvalid) {
return onInvalid()
}
return new Date()
}
return result
}
static formatDate (value: Date | number | string, format = 'y-m-d h:i:s') {
const date = this.correctDate(value)
const zero = (num: number) => parseInt(String(num)) < 10 ? '0' + num : num
const contrast: StringDict = {
y: date.getFullYear(),
m: zero(date.getMonth() + 1),
d: zero(date.getDate()),
h: zero(date.getHours()),
i: zero(date.getMinutes()),
s: zero(date.getSeconds())
}
for (const n in contrast) {
format = format.replace(new RegExp(n, 'g'), contrast[n])
}
return format
}
static deepCopy (obj: any) {
return JSON.parse(JSON.stringify(obj))
}
}

158
src/lib/http.ts Normal file
View File

@ -0,0 +1,158 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Notice from '@/lib/message'
import Common from '@/lib/common'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElLoadingService } from 'element-plus'
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
type RequestPrototype = 'FormData'
interface RequestConfig extends AxiosRequestConfig {
prototype?: RequestPrototype
complete?: () => void
}
interface HttpOptions {
host?: string
}
export class RequestCon {
static requesting = 0
static loadingInstance: LoadingInstance
static loading (text = '等待服务器处理...') {
this.requesting += 1
this.loadingInstance = ElLoadingService(
{
fullscreen: true,
text: text,
customClass: 'v-loading'
}
)
}
static closeLoading () {
this.requesting -= 1
if (this.requesting === 0) {
this.loadingInstance.close()
}
}
}
export default class HttpRequest {
private instance: AxiosInstance
constructor (options: HttpOptions = {}) {
this.instance = axios.create(
{
timeout: 200000,
withCredentials: false
}
)
this.instance.interceptors.response.use(this.onResponse, this.onResponseError)
this.instance.interceptors.request.use(conf => this.onRequest(conf, options))
}
onRequest (config: RequestConfig, options: HttpOptions) {
const host = options.host || Common.getData('host')
if (!host) {
return undefined
}
config.baseURL = host
config.headers = {
...{
token: Common.getData('token') || ''
},
...config.headers || {}
}
config.headers['cf-access-client-id'] = Common.getData('cf_id') || ''
config.headers['cf-access-client-secret'] = Common.getData('cf_token') || ''
/**
* data
*/
const configData = {
...config.data,
...config.params
}
if (Object.keys(configData).length) {
if (config.prototype === 'FormData') {
const data = new FormData()
for (const key in configData) {
data.append(key, configData[key])
}
config.data = data
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
} else {
switch (config.method?.toLowerCase()) {
case 'post':
config.data = JSON.stringify(config.data)
config.headers['Content-Type'] = 'application/json'
break
case 'get':
config.params = configData
break
}
}
}
RequestCon.loading()
return config
}
onResponse (response: AxiosResponse) {
const data = response.data
RequestCon.closeLoading()
switch (data.code) {
case 200:
if (data.message) {
Notice.notify(data.message, '提示', 'success')
}
break
case 500:
Notice.notify(data.message, '操作未成功', 'error')
return undefined
}
return data
}
onResponseError (error: AxiosError) {
const response = error.response
let errorMessage = ''
if (response?.status) {
errorMessage = `${response?.config.url}<br>Code: ${response?.status} ${response?.statusText}<br><span style="color: #f44336">${JSON.stringify(response?.data)}</span>`
} else {
errorMessage = '接口请求失败'
}
RequestCon.closeLoading()
Notice.notify(errorMessage, error.code, 'error', 10000)
}
async request (options: RequestConfig): Promise<any> {
return await this.instance.request(options)
}
async get (options: RequestConfig) {
return await this.request({
...options,
method: 'get'
})
}
async post (options: RequestConfig) {
return await this.request({
...options,
method: 'post'
})
}
}

75
src/lib/message.ts Normal file
View File

@ -0,0 +1,75 @@
import {
ElMessage as Message,
ElMessageBox as MessageBox,
ElNotification as Notification
} from 'element-plus'
type NoticeTypes = 'success' | 'info' | 'warning' | 'error'
type Callback = () => void
function doNothing () {
//
}
export default class Notice {
static toast (text: string, type: NoticeTypes = 'success') {
Message(
{
type: type,
message: text
}
)
}
static notify (text: string, title = '提示', type: NoticeTypes = 'info', duration = 5000) {
Notification(
{
type: type,
title: title,
message: text,
duration: duration,
dangerouslyUseHTMLString: true
}
)
}
static async alert (text: string, title = '提示', callback: Callback = doNothing, type: NoticeTypes = 'info') {
return !!await MessageBox
.alert(text, title,
{
type: type,
confirmButtonText: '好的',
callback: callback
}
)
}
static async confirm (text: string, title = '提示', type: NoticeTypes = 'warning', button: Array<string> = ['确定', '取消']) {
try {
return !!await MessageBox
.confirm(text, title,
{
confirmButtonText: button[0],
cancelButtonText: button[1],
distinguishCancelAndClose: true,
type: type
}
)
} catch (e) {
return false
}
}
static async prompt (text: string, title = '提示') {
const res = await MessageBox
.prompt(text, title,
{
confirmButtonText: '确定',
cancelButtonText: '取消'
}
)
if (res) {
return res.value
}
}
}

12
src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
import '@/assets/css/github-markdown-light.css'
import '@/components/table/customTableStyle.scss'
const app = createApp(App)
app.use(router)
app.mount('#app')

15
src/request/dashboard.ts Normal file
View File

@ -0,0 +1,15 @@
import HttpRequest from '@/lib/http'
const request = new HttpRequest()
export async function getLog () {
return await request.get({
url: '/pagermaid/api/log'
})
}
export async function getBotStatus () {
return await request.get({
url: '/pagermaid/api/status'
})
}

23
src/request/ignore.ts Normal file
View File

@ -0,0 +1,23 @@
import HttpRequest from '@/lib/http'
import { StringDict } from '@/lib/common'
const request = new HttpRequest()
export async function getIgnoreList () {
return await request.get({
url: '/pagermaid/api/get_ignore_group_list'
})
}
export async function editIgnoreGroup (data: StringDict) {
return await request.post({
url: '/pagermaid/api/set_ignore_group_status',
data
})
}
export async function removeAllIgnore () {
return await request.post({
url: '/pagermaid/api/clear_ignore_group'
})
}

56
src/request/plugin.ts Normal file
View File

@ -0,0 +1,56 @@
import Notice from '@/lib/message'
import HttpRequest from '@/lib/http'
import { StringDict } from '@/lib/common'
const request = new HttpRequest()
export async function getInstalledPlugin () {
return await request.get({
url: '/pagermaid/api/get_local_plugins'
})
}
export async function setLocalPluginStatus (data: StringDict) {
if (await Notice.confirm('确定改变插件【' + data.plugin + '】的状态?')) {
data.status = !data.status
return await request.post({
url: '/pagermaid/api/set_local_plugin_status',
data
})
}
}
export async function uninstallLocalPlugin (data: StringDict) {
if (await Notice.confirm('确定卸载插件【' + data.plugin + '】?')) {
return await request.post({
url: '/pagermaid/api/remove_local_plugin',
data
})
}
}
export async function getRemotePlugin () {
return await request.get({
url: '/pagermaid/api/get_remote_plugins'
})
}
export async function setRemotePluginStatus (data: StringDict) {
if (await Notice.confirm('确定改变插件【' + data.plugin + '】的状态?')) {
data.status = !data.status
return await request.post({
url: '/pagermaid/api/set_remote_plugin_status',
data
})
}
}
export async function upgradePlugin (data: StringDict) {
if (await Notice.confirm('确定更新插件【' + data.plugin + '】?')) {
data.status = true
return await request.post({
url: '/pagermaid/api/set_remote_plugin_status',
data
})
}
}

View File

@ -0,0 +1,11 @@
import HttpRequest from '@/lib/http'
export const cos = new HttpRequest({
host: '//'
})
export async function getNotice () {
return await cos.get({
url: '/notice.txt'
})
}

19
src/request/replace.ts Normal file
View File

@ -0,0 +1,19 @@
import HttpRequest from '@/lib/http'
import { StringDict } from '@/lib/common'
const request = new HttpRequest()
export async function getReplaceList () {
return await request.get({
url: '/pagermaid/api/command_alias'
})
}
export async function updateReplaceList (data: StringDict) {
return await request.post({
url: 'pagermaid/api/command_alias',
data: {
items: data
}
})
}

38
src/request/shell.ts Normal file
View File

@ -0,0 +1,38 @@
import HttpRequest from '@/lib/http'
import Notice from '@/lib/message'
const request = new HttpRequest()
export async function runEval (cmd: string) {
return await request.get({
params: {
cmd: cmd
},
url: '/pagermaid/api/run_eval'
})
}
export async function runSh (cmd: string) {
return await request.get({
params: {
cmd: cmd
},
url: '/pagermaid/api/run_sh'
})
}
export async function botUpdate () {
if (await Notice.confirm('确定更新吗?')) {
return await request.post({
url: '/pagermaid/api/bot_update'
})
}
}
export async function botRestart () {
if (await Notice.confirm('确定重启吗?')) {
return await request.post({
url: '/pagermaid/api/bot_restart'
})
}
}

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

@ -0,0 +1,54 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const indexChildren: Array<RouteRecordRaw> = [
{
path: '/',
name: 'index',
component: () => import('../views/app/dashboard.vue')
},
{
path: '/shell',
name: 'shell',
component: () => import('../views/app/shell.vue')
},
{
path: '/replace',
name: 'replace',
component: () => import('../views/app/replace.vue')
},
{
path: '/ignore',
name: 'ignore',
component: () => import('../views/app/ignore.vue')
},
{
path: '/plugin',
name: 'plugin',
component: () => import('../views/app/plugin.vue')
},
{
path: '/shop',
name: 'shop',
component: () => import('../views/app/shop.vue')
}
]
const routes: Array<RouteRecordRaw> = [
{
path: '/main',
name: 'main',
component: () => import('../views/main/main.vue'),
children: indexChildren
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export {
indexChildren
}
export default router

6
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type {DefineComponent} from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -0,0 +1,54 @@
<template>
<div>
<el-alert type="warning" v-if="notice">
<template #title>
<div style="display: flex;align-items: center;">
<el-icon>
<BellFilled />
</el-icon>
<span style="padding-left: 5px">公告</span>
</div>
</template>
<div v-html="notice"></div>
</el-alert>
<br>
<status />
<br>
<logger />
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { BellFilled } from '@element-plus/icons-vue'
import Logger from '@/views/app/dashboardElem/logger.vue'
import Status from '@/views/app/dashboardElem/status.vue'
import { getNotice } from '@/request/remote/notice'
@Options({
components: {
Logger,
Status,
BellFilled
},
mounted () {
this.getNotice()
}
})
export default class Dashboard extends Vue {
public notice = ''
public async getNotice () {
const res = await getNotice()
if (res) {
this.notice = res
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,146 @@
<template>
<el-card class="log">
<template #header>
<div class="log-header">
<div style="display: flex;align-items: center;">
<span>运行日志&nbsp;&nbsp;</span>
<el-icon style="color: var(--el-color-success); font-size: 18px; cursor: pointer"
v-if="!refresh"
@click="getLog">
<VideoPlay/>
</el-icon>
<el-icon style="color: var(--el-color-danger); font-size: 18px; cursor: pointer"
v-else
@click="refresh = false">
<VideoPause/>
</el-icon>
</div>
</div>
</template>
<div class="log-pad" ref="pad">
<div v-for="(line, index) in logList" :key="index" v-html="logFormat(line)"></div>
</div>
</el-card>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { getLog } from '@/request/dashboard'
import { VideoPause, VideoPlay } from '@element-plus/icons-vue'
import $ from 'jquery'
let loopLog: any = null
@Options({
components: {
VideoPause,
VideoPlay
},
computed: {
pad () {
return this.$refs.pad
}
},
watch: {
refresh (value: boolean) {
if (!value) {
clearInterval(loopLog)
} else {
this.getLog()
}
}
},
mounted () {
this.getLog()
$(this.$refs.pad).on('scroll', (e) => {
this.refresh = e.target.scrollTop + 400 >= e.target.scrollHeight
})
}
})
export default class Logger extends Vue {
pad!: HTMLElement
public logList = []
public refresh = true
public async getLog () {
clearInterval(loopLog)
if (this.$route.fullPath !== '/') {
this.refresh = false
return
}
const res = await getLog()
if (res) {
this.refresh = true
this.logList = res.split('\n')
await this.$nextTick(() => {
this.pad.scrollTop = this.pad.scrollHeight
})
loopLog = setInterval(this.getLog, 100000)
} else {
this.refresh = false
}
}
public logFormat (line: string): string {
return line
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace(/\s/g, '&nbsp;')
.replace(/&nbsp;\[(.*)]\[(.+?)]&nbsp;/, (res, r1, r2) => {
const main = r1.replace(/&nbsp;/g, '')
const level = r2.replace(/&nbsp;/g, '')
return `<span class="tag-main ${main}">${main}</span><span class="tag-level ${level}">${level}</span>`
})
}
}
</script>
<style scoped lang="scss">
.log {
width: 100%;
.log-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.log-pad {
height: 400px;
overflow: auto;
padding: 5px;
& > div {
font-size: 12px;
white-space: nowrap;
}
}
}
</style>
<style lang="scss">
.log-pad {
span {
width: 60px;
margin-right: 5px;
font-size: 12px;
background: var(--el-color-info-light-9);
padding: 0 5px;
display: inline-block;
text-align: right;
}
.tag-main {
margin-left: 5px;
}
.tag-level.ERROR {
background-color: var(--el-color-danger-light-5);
}
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div class="number-card" :style="{ backgroundColor: '#' + color }">
<div class="value">{{ value }}</div>
<div class="title">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
@Options({
props: {
title: String,
value: String,
color: String
}
})
export default class NumberCard extends Vue {
}
</script>
<style scoped lang="scss">
.number-card {
height: 60px;
padding: 0 20px;
color: #fff;
margin-right: 15px;
border-radius: 12px;
display: flex;
.value {
line-height: 60px;
font-size: 30px;
}
.title {
padding-left: 10px;
font-size: 15px;
flex-direction: column;
justify-content: center;
display: flex;
}
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div class="status">
<el-card>
<div style="display: flex">
<number-card :value="status.version" color="4caf50">
<template #default>
<span>Bot 版本</span>
</template>
</number-card>
<number-card :value="status.run_time" color="5f75ed">
<template #default>
<span>运行时间</span>
</template>
</number-card>
<number-card :value="status.cpu_percent" color="4caf50">
<template #default>
<span>CPU 占用率</span>
</template>
</number-card>
<number-card :value="status.ram_percent" color="4caf50">
<template #default>
<span>RAM 占用率</span>
</template>
</number-card>
<number-card :value="status.swap_percent" color="4caf50">
<template #default>
<span>SWAP 占用率</span>
</template>
</number-card>
</div>
</el-card>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { StringDict } from '@/lib/common'
import NumberCard from '@/views/app/dashboardElem/numberCard.vue'
import { getBotStatus } from '@/request/dashboard'
@Options({
components: {
NumberCard
},
mounted () {
this.getData()
}
})
export default class Status extends Vue {
public status: StringDict = {}
public async getData () {
const res = await getBotStatus()
if (res) {
this.status = res
}
}
}
</script>
<style scoped lang="scss">
.status {}
</style>

104
src/views/app/ignore.vue Normal file
View File

@ -0,0 +1,104 @@
<template>
<div>
<v-table ref="table" :load="loadList">
<template #header>
<el-button type="danger" @click="removeAllIgnore">清空忽略状态</el-button>
</template>
<template #operations="{row}">
<el-link :underline="false" type="primary" @click="editStatus(row)">编辑</el-link>
</template>
</v-table>
<v-form-dialog title="编辑状态" :form="form" ref="dialog">
<el-form-item label="群组 ID">
<el-input v-model="form.id" disabled/>
</el-form-item>
<el-form-item label="群组昵称">
<el-input v-model="form.title" disabled/>
</el-form-item>
<el-form-item label="是否屏蔽">
<el-switch v-model="form.status" :active-value="true" :inactive-value="false"/>
</el-form-item>
<template #footer>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</v-form-dialog>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { getIgnoreList, editIgnoreGroup, removeAllIgnore } from '@/request/ignore'
import Common, { DictArray, StringDict } from '@/lib/common'
import VTable from '@/components/table/v-table.vue'
import VFormDialog from '@/components/v-form-dialog.vue'
@Options({
components: {
VTable,
VFormDialog
},
computed: {
table () {
return this.$refs.table
},
dialog () {
return this.$refs.dialog
}
},
mounted () {
this.table.setColumns({
title: '群组名称',
id: '群组 ID',
status: {
title: '状态',
format: (row: StringDict, value: any) => value === true ? '已屏蔽' : '未屏蔽'
}
})
}
})
export default class User extends Vue {
table!: VTable
dialog!: VFormDialog
private form: StringDict = {}
private ignores: DictArray = []
public async loadList () {
const res = await getIgnoreList()
if (res) {
this.ignores = res.data.groups
const tableData = {
total: this.ignores.length,
list: this.ignores
}
this.table.setPaginationData(tableData)
}
}
public async editStatus (data: StringDict) {
this.form = Common.deepCopy(data)
this.dialog.show()
}
public async submit () {
const res = await editIgnoreGroup(this.form)
if (res) {
this.dialog.hide()
this.table.executeLoad()
}
}
public async removeAllIgnore () {
const res = await removeAllIgnore()
if (res) {
this.table.executeLoad()
}
}
}
</script>
<style scoped lang="scss">
</style>

71
src/views/app/plugin.vue Normal file
View File

@ -0,0 +1,71 @@
<template>
<div>
<div class="plugin-list" v-if="pluginList.length">
<plugin-item-card v-for="(item, index) in pluginList" :key="index" :item="item">
<template #button>
<el-button round type="danger" @click="setStatus(item)" v-if="item.status">禁用</el-button>
<el-button round type="primary" @click="setStatus(item)" v-else>启用</el-button>
<el-button round type="danger" @click="uninstall(item)">卸载</el-button>
</template>
</plugin-item-card>
</div>
<div v-else>
<el-empty description="未安装任何插件"/>
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { getInstalledPlugin, setLocalPluginStatus, uninstallLocalPlugin } from '@/request/plugin'
import PluginItemCard, { PluginItem } from '@/views/app/pluginElem/pluginItemCard.vue'
@Options({
components: {
PluginItemCard
},
mounted () {
this.getPlugin()
}
})
export default class Plugin extends Vue {
public pluginList = []
public plugin: PluginItem = {
name: '',
status: false,
installed: false,
version: 1.0
}
public async getPlugin () {
const res = await getInstalledPlugin()
if (res) {
this.pluginList = res.data.rows
}
}
public async setStatus (item: PluginItem) {
item.plugin = item.name
const res = await setLocalPluginStatus(item)
if (res) {
await this.getPlugin()
}
}
public async uninstall (item: PluginItem) {
item.plugin = item.name
const res = await uninstallLocalPlugin(item)
if (res) {
await this.getPlugin()
}
}
}
</script>
<style scoped lang="scss">
.plugin-list {
display: flex;
flex-wrap: wrap;
}
</style>

View File

@ -0,0 +1,180 @@
<template>
<div class="plugin-item">
<div class="plugin-title" @click="dialog.show()">
<div class="plugin-icon">
<img src="../../../assets/icon/plugin.svg" alt="logo">
</div>
<div class="plugin-info">
<div class="plugin-name">
<div>{{ item.name }}</div>
</div>
<div style="color: var(--el-color-primary)">
<slot name="version">{{ item.version }}</slot>
</div>
</div>
</div>
</div>
<v-dialog custom-class="plugin-detail" ref="dialog" :append-to-body="true" :show-close="false" width="60%">
<div class="plugin-detail-header">
<div class="plugin-detail-title">
<div class="plugin-icon detail">
<img src="../../../assets/icon/plugin.svg" alt="logo">
</div>
<div>
<div class="plugin-name detail">
<div style="color: var(--el-color-primary)">{{ item.name }}</div>
</div>
<div class="plugin-detail-info">
<div>
<el-icon>
<Discount />
</el-icon>
<div>版本</div>
<slot name="version">{{ item.version }}</slot>
</div>
</div>
</div>
</div>
<div>
<slot name="button"></slot>
</div>
</div>
</v-dialog>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { StringDict } from '@/lib/common'
import { Discount, User, Download } from '@element-plus/icons-vue'
import VDialog from '@/components/v-dialog.vue'
export interface PluginItem extends StringDict {
'name': string
'version': number
'status': boolean
'installed': boolean
}
@Options({
components: {
VDialog,
Discount,
User,
Download
},
computed: {
dialog () {
return this.$refs.dialog
}
},
props: {
item: Object,
author: String,
downloadCount: Number
}
})
export default class PluginItemCard extends Vue {
item!: PluginItem
dialog!: VDialog
}
</script>
<style scoped lang="scss">
.plugin-item {
width: 240px;
margin: 0 10px 10px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 150ms ease-in-out;
.plugin-title {
display: flex;
align-items: center;
cursor: pointer;
.plugin-info {
display: flex;
flex-direction: column;
}
}
}
.plugin-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
.plugin-detail-title {
display: flex;
align-items: center;
}
}
.plugin-icon {
width: 35px;
height: 35px;
margin-right: 10px;
border-radius: 4px;
border: 1px solid var(--el-card-border-color);
box-shadow: var(--el-box-shadow-light);
display: flex;
align-items: center;
overflow: hidden;
&.detail {
width: 50px;
height: 50px;
}
img {
width: 100%;
}
}
.plugin-name {
display: flex;
align-items: center;
&.detail>div {
font-size: 20px;
}
}
.plugin-detail-info {
display: flex;
&>div {
margin-top: 5px;
margin-right: 10px;
padding-right: 10px;
border-right: 1px solid var(--el-border-color);
display: flex;
align-items: center;
.el-icon {
font-size: 14px;
margin-right: 3px;
}
}
&>div:last-child {
border-right: none;
}
}
</style>
<style lang="scss">
.plugin-detail {
&>header,
&>footer {
display: none;
}
.el-dialog__body {
color: var(--el-color-black);
}
}
</style>

View File

@ -0,0 +1,212 @@
<template>
<div class="plugin-item">
<div class="plugin-title" @click="dialog.show()">
<div class="plugin-icon">
<img src="../../../assets/icon/plugin.svg" alt="logo">
</div>
<div class="plugin-info">
<div class="plugin-name">
<div>{{ item.name }}</div>
</div>
<div style="color: var(--el-color-primary)">
<slot name="version">{{ item.version }}</slot>
</div>
</div>
</div>
</div>
<v-dialog custom-class="plugin-detail" ref="dialog" :append-to-body="true" :show-close="false" width="60%">
<div class="plugin-detail-header">
<div class="plugin-detail-title">
<div class="plugin-icon detail">
<img src="../../../assets/icon/plugin.svg" alt="logo">
</div>
<div>
<div class="plugin-name detail">
<div style="color: var(--el-color-primary)">{{ item.name }}</div>
</div>
<div class="plugin-detail-info">
<div>
<el-icon>
<Discount/>
</el-icon>
<div>版本</div>
<slot name="version">{{ item.version }}</slot>
</div>
<div>
<el-icon>
<User/>
</el-icon>
<div>作者</div>
<div>
<slot>{{ item.maintainer }}</slot>
</div>
</div>
</div>
</div>
</div>
<div>
<slot name="button"></slot>
</div>
</div>
<el-divider content-position="left">插件介绍</el-divider>
<div class="plugin-doc">
<div class="markdown-body" v-html="pluginDoc()"></div>
</div>
</v-dialog>
</template>
<script lang="ts">
import { marked } from 'marked'
import { Options, Vue } from 'vue-class-component'
import { StringDict } from '@/lib/common'
import { Discount, User, Download } from '@element-plus/icons-vue'
import VDialog from '@/components/v-dialog.vue'
export interface RemotePluginItem extends StringDict {
'name': string
'version': number
'status': boolean
'installed': boolean
'section': string
'maintainer': string
'size': string
'supported': boolean
'des': string
'upgrade': boolean
}
@Options({
components: {
VDialog,
Discount,
User,
Download
},
computed: {
dialog () {
return this.$refs.dialog
}
},
props: {
item: Object,
author: String,
downloadCount: Number
}
})
export default class PluginItemCard extends Vue {
item!: RemotePluginItem
dialog!: VDialog
public pluginDoc () {
return marked.parse(this.item.des)
}
}
</script>
<style scoped lang="scss">
.plugin-item {
width: 240px;
margin: 0 10px 10px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 150ms ease-in-out;
.plugin-title {
display: flex;
align-items: center;
cursor: pointer;
.plugin-info {
display: flex;
flex-direction: column;
}
}
}
.plugin-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
.plugin-detail-title {
display: flex;
align-items: center;
}
}
.plugin-icon {
width: 35px;
height: 35px;
margin-right: 10px;
border-radius: 4px;
border: 1px solid var(--el-card-border-color);
box-shadow: var(--el-box-shadow-light);
display: flex;
align-items: center;
overflow: hidden;
&.detail {
width: 50px;
height: 50px;
}
img {
width: 100%;
}
}
.plugin-name {
display: flex;
align-items: center;
&.detail > div {
font-size: 20px;
}
}
.plugin-desc {
margin: 30px 0;
}
.plugin-detail-info {
display: flex;
& > div {
margin-top: 5px;
margin-right: 10px;
padding-right: 10px;
border-right: 1px solid var(--el-border-color);
display: flex;
align-items: center;
.el-icon {
font-size: 14px;
margin-right: 3px;
}
}
& > div:last-child {
border-right: none;
}
}
.plugin-doc {
max-height: 400px;
overflow: auto;
}
</style>
<style lang="scss">
.plugin-detail {
& > header,
& > footer {
display: none;
}
.el-dialog__body {
color: var(--el-color-black);
}
}
</style>

142
src/views/app/replace.vue Normal file
View File

@ -0,0 +1,142 @@
<template>
<div>
<v-table ref="table" :load="loadList">
<template #header>
<el-button type="primary" @click="$refs.dialog.show()">添加命令别名</el-button>
<el-button type="warning" @click="loadList">同步命令别名</el-button>
</template>
<template #operations="{row}">
<el-link :underline="false" type="primary" @click="editReplace(row)">编辑</el-link>
<el-link :underline="false" type="danger" @click="delReplace(row)">删除</el-link>
</template>
</v-table>
<v-form-dialog title="添加命令别名" :form="form" ref="dialog">
<el-form-item label="被替换命令">
<el-input v-model="form.command"/>
</el-form-item>
<el-form-item label="替换命令">
<el-input v-model="form.alias"/>
</el-form-item>
<template #footer>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</v-form-dialog>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import {
getReplaceList, updateReplaceList
} from '@/request/replace'
import { DictArray, StringDict } from '@/lib/common'
import VTable from '@/components/table/v-table.vue'
import VFormDialog from '@/components/v-form-dialog.vue'
@Options({
components: {
VTable,
VFormDialog
},
computed: {
table () {
return this.$refs.table
},
dialog () {
return this.$refs.dialog
},
setting () {
return this.$refs.setting
}
},
watch: {
inputMode (val) {
if (val) {
this.$refs.tagInput.focus()
}
}
},
mounted () {
this.table.setColumns({
command: '被替换命令',
alias: '替换命令'
})
}
})
export default class Replace extends Vue {
table!: VTable
dialog!: VFormDialog
setting!: VFormDialog
private form = {
command: '',
alias: ''
}
private alias: DictArray = []
public async loadList () {
const res = await getReplaceList()
if (res) {
this.alias = res.data.items
const tableData = {
total: this.alias.length,
list: this.alias
}
this.table.setPaginationData(tableData)
}
}
public async submit () {
const newAlias = {
command: this.form.command,
alias: this.form.alias
}
this.alias.push(newAlias)
const res = await updateReplaceList(this.alias)
if (res) {
this.dialog.hide()
this.table.executeLoad()
this.form = {
command: '',
alias: ''
}
}
}
public async delReplace (data: StringDict) {
this.alias = this.alias.filter((item) => {
return item.command !== data.command
})
const res = await updateReplaceList(this.alias)
if (res) {
this.table.executeLoad()
}
}
public async editReplace (data: StringDict) {
this.form = {
command: data.command,
alias: data.alias
}
this.dialog.show()
}
}
</script>
<style scoped lang="scss">
.setting-word {
display: flex;
flex-wrap: wrap;
.tag {
margin-right: 5px;
}
.add {
width: 100px;
}
}
</style>

163
src/views/app/shell.vue Normal file
View File

@ -0,0 +1,163 @@
<template>
<div>
<el-button type="primary" @click="update">更新</el-button>
<el-button type="danger" @click="restart">重启</el-button>
<el-button type="warning" @click="$refs.shell.show()">运行 Shell 命令</el-button>
<el-button type="warning" @click="$refs.eval.show()">运行 python 命令</el-button>
<br><br>
<el-card class="log">
<template #header>
<div class="log-header">
<div style="display: flex;align-items: center;">
<span>运行日志&nbsp;&nbsp;</span>
</div>
</div>
</template>
<div class="log-pad" ref="pad">
<div v-for="(line, index) in logList" :key="index" v-html="logFormat(line)"></div>
</div>
</el-card>
<v-form-dialog title="运行 Shell 命令" :form="form" ref="shell">
<el-form-item label="命令">
<el-input v-model="form.cmd" :rows="8" type="textarea"/>
</el-form-item>
<template #footer>
<el-button type="primary" @click="submit(false)">提交</el-button>
</template>
</v-form-dialog>
<v-form-dialog title="运行 python 命令" :form="form" ref="eval">
<el-form-item label="命令">
<el-input v-model="form.cmd" :rows="8" type="textarea"/>
</el-form-item>
<template #footer>
<el-button type="primary" @click="submit(true)">提交</el-button>
</template>
</v-form-dialog>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { runEval, runSh, botUpdate, botRestart } from '@/request/shell'
import VFormDialog from '@/components/v-form-dialog.vue'
@Options({
components: {
VFormDialog
},
computed: {
table () {
return this.$refs.table
},
shell () {
return this.$refs.shell
},
eval () {
return this.$refs.eval
},
pad () {
return this.$refs.pad
}
},
watch: {
inputMode (val) {
if (val) {
this.$refs.tagInput.focus()
}
}
}
})
export default class Shell extends Vue {
shell!: VFormDialog
eval!: VFormDialog
pad!: HTMLElement
public logList: any = []
private form = {
cmd: ''
}
public async submit (type: boolean) {
let res = ''
if (type) {
res = await runEval(this.form.cmd)
} else {
res = await runSh(this.form.cmd)
}
if (res) {
this.shell.hide()
this.eval.hide()
this.logList = res.split('\n')
await this.$nextTick(() => {
this.pad.scrollTop = this.pad.scrollHeight
})
}
}
public logFormat (line: string): string {
return line
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace(/\s/g, '&nbsp;')
.replace(/&nbsp;\[(.*)]\[(.+?)]&nbsp;/, (res, r1, r2) => {
const main = r1.replace(/&nbsp;/g, '')
const level = r2.replace(/&nbsp;/g, '')
return `<span class="tag-main ${main}">${main}</span><span class="tag-level ${level}">${level}</span>`
})
}
public async update () {
return await botUpdate()
}
public async restart () {
return await botRestart()
}
}
</script>
<style scoped lang="scss">
.log {
width: 100%;
.log-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.log-pad {
height: 400px;
overflow: auto;
padding: 5px;
& > div {
font-size: 12px;
white-space: nowrap;
}
}
}
</style>
<style lang="scss">
.log-pad {
span {
width: 60px;
margin-right: 5px;
font-size: 12px;
background: var(--el-color-info-light-9);
padding: 0 5px;
display: inline-block;
text-align: right;
}
.tag-main {
margin-left: 5px;
}
}
</style>

113
src/views/app/shop.vue Normal file
View File

@ -0,0 +1,113 @@
<template>
<div>
<div style="margin-bottom: 10px">
<el-button type="success" @click="upgradeAll" v-if="needUpdate">更新所有插件</el-button>
</div>
<div class="plugin-list">
<plugin-item-card v-for="(item, index) in officialPlugins" :key="index" :item="item">
<template #version>
<div style="display: flex;align-items: center;">
{{ item.version }}
</div>
</template>
<template #button>
<el-button round type="success" @click="upgrade(item)" v-if="item.upgrade">更新</el-button>
<el-button round type="primary" @click="setStatus(item)" v-if="!item.status">安装</el-button>
<el-button round type="danger" @click="setStatus(item)" style="margin-left: 10px" v-else>
卸载
</el-button>
</template>
</plugin-item-card>
</div>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { getInstalledPlugin, getRemotePlugin, setRemotePluginStatus, upgradePlugin } from '@/request/plugin'
import { StringDict } from '@/lib/common'
import { CaretTop, CaretBottom } from '@element-plus/icons-vue'
import PluginItemCard, { RemotePluginItem } from '@/views/app/pluginElem/remotePluginItemCard.vue'
import { PluginItem } from './pluginElem/pluginItemCard.vue'
@Options({
components: {
CaretTop,
CaretBottom,
PluginItemCard
},
mounted () {
this.getShopList()
}
})
export default class Shop extends Vue {
public officialPlugins: Array<RemotePluginItem> = []
public needUpdate = false
public async getShopList () {
this.needUpdate = false
const shop = await getRemotePlugin()
if (shop) {
this.officialPlugins = shop.data.rows
const res = await getInstalledPlugin()
if (res) {
const list: Array<PluginItem> = res.data.rows
const installedPlugin: StringDict = {}
for (const item of list) {
installedPlugin[item.name] = item.version
}
for (const item of this.officialPlugins) {
item.upgrade = false
if (installedPlugin[item.name]) {
if (installedPlugin[item.name] < item.version) {
item.upgrade = true
this.needUpdate = true
}
}
}
}
}
}
public async setStatus (item: StringDict) {
item.plugin = item.name
const res = await setRemotePluginStatus(item)
if (res) {
await this.getShopList()
}
}
public async upgrade (item: StringDict) {
item.plugin = item.name
const res = await upgradePlugin(item)
if (res) {
await this.getShopList()
}
}
public async upgradeAll () {
const upgradePlugins = this.officialPlugins.filter((item: RemotePluginItem) => { return item.upgrade })
for (const item of upgradePlugins) {
item.plugin = item.name
await upgradePlugin(item)
}
await this.getShopList()
}
}
</script>
<style scoped lang="scss">
.plugin-author {
font-size: 16px;
border-left: 3px solid var(--el-color-success);
padding-left: 10px;
margin-top: 20px;
}
.plugin-list {
padding: 10px;
display: flex;
flex-wrap: wrap;
}
</style>

164
src/views/main/main.scss Normal file
View File

@ -0,0 +1,164 @@
.header {
height: 50px;
padding: 0 20px;
color: #fff;
background: var(--c-main);
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 20px;
display: flex;
align-items: center;
}
.options {
display: flex;
& > div {
margin-right: 20px;
display: flex;
align-items: center;
}
& > div:not(.connect) {
cursor: pointer;
}
.icon-book {
width: 25px;
height: 25px;
margin-right: 3px;
background: url(../../assets/icon/book.svg) center / 25px no-repeat;
}
.icon-github {
width: 25px;
height: 25px;
margin-right: 3px;
background: url(../../assets/icon/github.svg) center / 20px no-repeat;
}
.status {
width: 10px;
height: 10px;
margin-right: 5px;
border-radius: 50%;
background: #67c23a;
display: inline-block;
&.off {
background: #ff9800;
}
}
}
}
.body {
height: calc(100% - 50px);
display: flex;
& > div {
height: 100%;
padding: 20px;
}
.nav-con {
width: 260px;
}
.app-con {
width: calc(100% - 260px);
& > div:first-child {
height: 40px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
& > div:last-child {
height: calc(100% - 55px);
position: relative;
}
& > div:last-child > div {
width: 100%;
height: 100%;
padding: 10px;
position: absolute;
top: 0;
left: 0;
overflow: auto;
}
.options-con {
height: 100%;
padding: 0 10px;
border-radius: 4px;
box-shadow: 0 0 12px rgb(0 0 0 / 12%);
display: flex;
align-items: center;
}
.icon {
width: 25px;
height: 25px;
margin: 0 3px;
background: center / 100% no-repeat;
border-radius: 4px;
cursor: pointer;
}
.icon:hover {
background-color: var(--c-main-light);
}
.bot {
background-image: url(../../assets/icon/robot.svg);
}
.setting {
background-image: url(../../assets/icon/set.svg);
}
}
.setting-header {
font-size: 16px;
display: flex;
align-items: center;
}
}
.no-setting {
width: 100%;
padding-top: 100px;
display: flex;
justify-content: center;
.setting-card {
width: 500px;
}
}
.slide-fade-enter-active {
transition: all 300ms ease-in-out;
transition-delay: 400ms;
}
.slide-fade-leave-active {
transition: all 300ms ease-in-out;
}
.slide-fade-enter-from {
transform: translateX(-20px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}

200
src/views/main/main.vue Normal file
View File

@ -0,0 +1,200 @@
<template>
<div class="header">
<div class="title">
PagerMaid-Pyro Web Console
<el-tag style="margin-left: 5px" type="danger" effect="dark" size="small">Beta</el-tag>
</div>
<div class="options">
<div onclick="window.open('https://xtaolabs.com/')"><span class="icon-book"></span>文档</div>
<div onclick="window.open('https://github.com/TeamPGM')">
<span class="icon-github"></span>Github
</div>
<div class="connect">
<div class="status" :class="{ off: !host() }"></div>
<div v-if="host()" style="display: flex">
Connected@
<el-link :underline="false" :href="originHost() + '/docs'" target="_blank" style="color: #fff">
{{ host() }}
</el-link>
</div>
<div v-else>Unconnected</div>
</div>
</div>
</div>
<div class="body" v-if="ready">
<div class="nav-con">
<main-nav @on-selected="name => this.name = name"></main-nav>
</div>
<div class="app-con">
<div>
<div style="font-size: 20px;display: flex;">
<el-icon class="icon">
<component :is="navIcon[name]"></component>
</el-icon>
{{ navName[name] }}
</div>
<div class="options-con">
<div class="icon bot"></div>
<div class="icon setting" @click="setting = true"></div>
</div>
</div>
<div>
<router-view v-slot="{ Component }">
<transition name="slide-fade">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
<el-drawer v-model="setting">
<template #header>
<div class="setting-header">
<el-icon style="margin-right: 5px">
<Setting />
</el-icon>
设置中心
</div>
</template>
<el-card>
<template #header>
<span>服务设置</span>
</template>
<el-form :model="settingForm" label-position="top">
<el-form-item label="PagerMaid-Pyro 服务地址">
<el-input v-model="settingForm.host" placeholder="请填写服务地址" />
</el-form-item>
<el-form-item label="PagerMaid-Pyro 服务密匙">
<el-input v-model="settingForm.token" placeholder="请填写服务密匙" show-password />
</el-form-item>
<el-form-item label="Cloudflare 客户端 ID (可选)">
<el-input v-model="settingForm.cf_id" placeholder="请填写 Cloudflare 客户端 ID" />
</el-form-item>
<el-form-item label="Cloudflare 访问密匙(可选)">
<el-input v-model="settingForm.cf_token" placeholder="请填写 Cloudflare 访问密匙" show-password />
</el-form-item>
<el-button type="success" @click="saveServer">保存</el-button>
<el-button type="danger" @click="unlinkServer">注销</el-button>
</el-form>
</el-card>
</el-drawer>
</div>
<div class="no-setting" v-else>
<el-card class="setting-card">
<template #header>
<span>设置你的 PagerMaid-Pyro 服务</span>
</template>
<el-form :model="settingForm" label-position="top">
<el-form-item label="PagerMaid-Pyro 服务地址">
<el-input v-model="settingForm.host" placeholder="请填写服务地址" />
</el-form-item>
<el-form-item label="PagerMaid-Pyro 服务密匙">
<el-input v-model="settingForm.token" placeholder="请填写服务密匙" show-password />
</el-form-item>
<el-form-item label="Cloudflare 客户端 ID (可选)">
<el-input v-model="settingForm.cf_id" placeholder="请填写 Cloudflare 客户端 ID" />
</el-form-item>
<el-form-item label="Cloudflare 访问密匙(可选)">
<el-input v-model="settingForm.cf_token" placeholder="请填写 Cloudflare 访问密匙" show-password />
</el-form-item>
</el-form>
<el-button type="success" @click="start">开始连接</el-button>
</el-card>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import { Setting } from '@element-plus/icons-vue'
import MainNav, { navName, navIcon } from '@/views/main/mainNav.vue'
import Common from '@/lib/common'
import HttpRequest from '@/lib/http'
import Notice from '@/lib/message'
@Options({
components: {
MainNav,
Setting
},
methods: {
originHost () {
return Common.getData('host')
},
host () {
return (Common.getData('host') || '')
.replace('http://', '')
.replace('https://', '')
}
},
mounted () {
this.ready = !!this.settingForm.host
}
})
export default class Main extends Vue {
public name = ''
public navName = navName
public navIcon = navIcon
private ready = false
private setting = false
private settingForm = {
host: Common.getData('host') || '',
token: Common.getData('token') || '',
cf_id: Common.getData('cf_id') || '',
cf_token: Common.getData('cf_token') || ''
}
public async saveServer () {
Common.setData('host', this.settingForm.host)
Common.setData('token', this.settingForm.token)
Common.setData('cf_id', this.settingForm.cf_id)
Common.setData('cf_token', this.settingForm.cf_token)
location.reload()
}
public async unlinkServer () {
Common.removeData('host')
Common.removeData('token')
Common.removeData('cf_id')
Common.removeData('cf_token')
location.reload()
}
public async start () {
if (!this.settingForm.host.startsWith('http')) {
this.settingForm.host = 'http://' + this.settingForm.host
}
const request = new HttpRequest({
host: this.settingForm.host
})
Common.setData('cf_id', this.settingForm.cf_id)
Common.setData('cf_token', this.settingForm.cf_token)
const res = await request.post({
data: {
password: this.settingForm.token
},
url: '/pagermaid/api/login'
})
console.log(res)
if (res.status === 0) {
if (res.data.version != null && res.data.version >= 1300) {
Common.setData('host', this.settingForm.host)
Common.setData('token', this.settingForm.token)
this.ready = true
} else {
await Notice.alert('您的 PagerMaid-Pyro 版本过低,无法使用此网页控制台', '版本过低')
}
} else {
await Notice.alert('登录失败,可能是密匙错误或服务地址错误', '登录失败')
}
}
}
</script>
<style scoped lang="scss" src="./main.scss">
</style>

126
src/views/main/mainNav.vue Normal file
View File

@ -0,0 +1,126 @@
<template>
<div class="main-nav">
<div class="menu">
<div v-for="(item, index) in indexChildren" :key="index" :class="{ selected: isSelected(index) }"
@click="selectMenu(index)">
<el-icon class="icon">
<component :is="navIcon[item.name]"></component>
</el-icon>
{{ navName[item.name] || item.name }}
</div>
</div>
</div>
</template>
<script lang="ts">
import { shallowRef } from 'vue'
import { Options, Vue } from 'vue-class-component'
import { RouteRecordRaw } from 'vue-router'
import { indexChildren } from '@/router'
import {
Monitor,
DocumentCopy,
CreditCard,
Goods,
Link
} from '@element-plus/icons-vue'
const navName = {
index: '控制台',
shell: 'shell',
replace: '命令别名',
ignore: '忽略群组',
plugin: '插件管理',
shop: '插件仓库'
}
const navIcon = {
index: shallowRef(Monitor),
shell: shallowRef(Monitor),
replace: shallowRef(DocumentCopy),
ignore: shallowRef(CreditCard),
plugin: shallowRef(Link),
shop: shallowRef(Goods)
}
export {
navName,
navIcon
}
@Options({
watch: {
selected (index) {
const item = this.indexChildren[index]
this.$emit('onSelected', item.name)
this.$router.push(item.path)
}
},
mounted () {
if (this.$route.name) {
for (const index in this.indexChildren) {
const item = this.indexChildren[index]
if (this.$route.name === item.name) {
this.selected = parseInt(index)
break
}
}
}
}
})
export default class MainNav extends Vue {
public selected: null | number = null
public indexChildren: Array<RouteRecordRaw> = indexChildren
public navName = navName
public navIcon = navIcon
public selectMenu (index: number): void {
this.selected = index
}
public isSelected (index: number): boolean {
return index === this.selected
}
}
</script>
<style scoped lang="scss">
.main-nav {
height: 100%;
border-radius: 4px;
box-shadow: 0 0 12px rgba(0, 0, 0, .12);
overflow: hidden;
}
.menu {
display: flex;
flex-direction: column;
&>div {
height: 40px;
padding: 0 20px;
transition: all 200ms ease-in-out;
display: flex;
align-items: center;
cursor: pointer;
}
&>div.selected {
color: var(--c-main);
background: var(--c-main-light);
}
&>div:hover:not(.selected) {
color: #fff;
background: var(--c-main);
}
.icon {
margin-right: 8px;
}
}
</style>

43
tsconfig.json Normal file
View File

@ -0,0 +1,43 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.js",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

23
vue.config.js Normal file
View File

@ -0,0 +1,23 @@
const webpack = require('webpack')
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
]
}
})

7419
yarn.lock Normal file

File diff suppressed because it is too large Load Diff