mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-12 19:05:55 +02:00
unfinished WebUI
This commit is contained in:
parent
0ad1b3af4b
commit
070b34be8a
69 changed files with 13932 additions and 140 deletions
15
.editorconfig
Executable file
15
.editorconfig
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
block_comment_start = /*
|
||||||
|
block_comment_end = */
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[README.md]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,5 +2,6 @@
|
||||||
.idea
|
.idea
|
||||||
database.db
|
database.db
|
||||||
|
|
||||||
|
|
||||||
server/tmp/main
|
server/tmp/main
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
|
4
nginx-ui-frontend/.env.development
Normal file
4
nginx-ui-frontend/.env.development
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
VUE_APP_API_ROOT = /
|
||||||
|
VUE_APP_API_CLIENT = ''
|
||||||
|
VUE_APP_API_CLIENT_SECRET = ''
|
||||||
|
VUE_APP_RECAPTCHA_SITEKEY = ''
|
4
nginx-ui-frontend/.env.production
Normal file
4
nginx-ui-frontend/.env.production
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
VUE_APP_API_ROOT = mock
|
||||||
|
VUE_APP_API_CLIENT = ''
|
||||||
|
VUE_APP_API_CLIENT_SECRET = ''
|
||||||
|
VUE_APP_RECAPTCHA_SITEKEY = ''
|
23
nginx-ui-frontend/.gitignore
vendored
Normal file
23
nginx-ui-frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
29
nginx-ui-frontend/README.md
Normal file
29
nginx-ui-frontend/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# nginx-ui-frontend
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
9
nginx-ui-frontend/alias.config.js
Normal file
9
nginx-ui-frontend/alias.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const resolve = dir => require('path').join(__dirname, dir)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
nginx-ui-frontend/babel.config.js
Normal file
8
nginx-ui-frontend/babel.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
"@vue/cli-plugin-babel/preset"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
["import", {"libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true}, "syntax-dynamic-import"]
|
||||||
|
],
|
||||||
|
}
|
60
nginx-ui-frontend/package.json
Normal file
60
nginx-ui-frontend/package.json
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "nginx-ui-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^1.7.3",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"core-js": "^3.9.0",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.5.1",
|
||||||
|
"vuex": "^3.6.2",
|
||||||
|
"vuex-persist": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-plugin-import": "^1.13.3",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"less": "^3.11.1",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vue-cli-plugin-generate-build-id": "0.1.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
BIN
nginx-ui-frontend/public/favicon.ico
Normal file
BIN
nginx-ui-frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
18
nginx-ui-frontend/public/index.html
Normal file
18
nginx-ui-frontend/public/index.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
||||||
|
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
|
||||||
|
<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>
|
12
nginx-ui-frontend/src/App.vue
Normal file
12
nginx-ui-frontend/src/App.vue
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
23
nginx-ui-frontend/src/api/config.js
Normal file
23
nginx-ui-frontend/src/api/config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import http from "@/lib/http"
|
||||||
|
|
||||||
|
const base_url = '/config'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
get_list(params) {
|
||||||
|
return http.get(base_url + 's', {params: params})
|
||||||
|
},
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return http.get(base_url + '/' + id)
|
||||||
|
},
|
||||||
|
|
||||||
|
save(id = null, data) {
|
||||||
|
return http.post(base_url + (id ? '/' + id : ''), data)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy(id) {
|
||||||
|
return http.delete(base_url + '/' + id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
35
nginx-ui-frontend/src/api/domain.js
Normal file
35
nginx-ui-frontend/src/api/domain.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import http from "@/lib/http"
|
||||||
|
|
||||||
|
const base_url = '/domain'
|
||||||
|
|
||||||
|
const domain = {
|
||||||
|
get_list(params) {
|
||||||
|
return http.get(base_url + 's', {params: params})
|
||||||
|
},
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return http.get(base_url + '/' + id)
|
||||||
|
},
|
||||||
|
|
||||||
|
save(id = null, data) {
|
||||||
|
return http.post(base_url + (id ? '/' + id : ''), data)
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy(id) {
|
||||||
|
return http.delete(base_url + '/' + id)
|
||||||
|
},
|
||||||
|
|
||||||
|
enable(name) {
|
||||||
|
return http.post(base_url + '/' + name + '/enable')
|
||||||
|
},
|
||||||
|
|
||||||
|
disable(name) {
|
||||||
|
return http.post(base_url + '/' + name + '/disable')
|
||||||
|
},
|
||||||
|
|
||||||
|
get_template(name) {
|
||||||
|
return http.get('template/' + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default domain
|
7
nginx-ui-frontend/src/api/index.js
Normal file
7
nginx-ui-frontend/src/api/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import domain from "./domain"
|
||||||
|
import config from "./config"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
domain,
|
||||||
|
config
|
||||||
|
}
|
58
nginx-ui-frontend/src/assets/css/dark.less
Normal file
58
nginx-ui-frontend/src/assets/css/dark.less
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@import '~ant-design-vue/dist/antd.less';
|
||||||
|
@black_bg: #1e1f20;
|
||||||
|
@background-color-base: @black_bg;
|
||||||
|
@background-color-light: @black_bg;
|
||||||
|
@btn-default-bg: @black_bg;
|
||||||
|
@black_content: #28292c;
|
||||||
|
@text-color: #fff;
|
||||||
|
@text-color-secondary: #bdbdbd;
|
||||||
|
@heading-color: #fff;
|
||||||
|
|
||||||
|
@body-background: @black_bg;
|
||||||
|
@component-background: @black_content;
|
||||||
|
@layout-body-background: @black_bg;
|
||||||
|
@layout-header-background: @black_bg;
|
||||||
|
@layout-sider-background: @black_content;
|
||||||
|
@ant-layout-sider-light: @black_content;
|
||||||
|
@menu-bg: @black_content;
|
||||||
|
@layout-trigger-background: @black_content;
|
||||||
|
@layout-sider-background-light: @black_content;
|
||||||
|
@layout-trigger-background-light: @black_content;
|
||||||
|
|
||||||
|
@item-active-bg: #161717;
|
||||||
|
|
||||||
|
@link-color: #fff;
|
||||||
|
@table-row-hover-bg: @black_bg;
|
||||||
|
@table-selected-row-bg: @black_bg;
|
||||||
|
|
||||||
|
@input-bg: @black_bg;
|
||||||
|
@disabled-bg: #363636;
|
||||||
|
@btn-default-bg: @black_bg;
|
||||||
|
@table-header-bg: @black_bg;
|
||||||
|
@border-color-base: #666666;
|
||||||
|
@disabled-color: #bcbcbc;
|
||||||
|
@radio-button-bg: @black_bg;
|
||||||
|
@checkbox-check-color: @black_bg;
|
||||||
|
@popover-bg: @black_bg;
|
||||||
|
|
||||||
|
.ant-select-dropdown-menu-item:hover {
|
||||||
|
background: @black_bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider {
|
||||||
|
background-color: @black_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-indeterminate {
|
||||||
|
.ant-checkbox-inner {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-transfer-list-content-item:not(.ant-transfer-list-content-item-disabled):hover,
|
||||||
|
.ant-transfer-list-content-item-highlight-enter, .ant-transfer-list-content-item-highlight-enter-active {
|
||||||
|
background-color: @black_bg !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
49
nginx-ui-frontend/src/assets/css/manage.less
Normal file
49
nginx-ui-frontend/src/assets/css/manage.less
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/* Copyright 2019 - 2020 0xJacky */
|
||||||
|
|
||||||
|
@dark: ~"(prefers-color-scheme: dark)";
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider {
|
||||||
|
background-color: #ffffff;
|
||||||
|
@media @dark {
|
||||||
|
background-color: #28292c;
|
||||||
|
}
|
||||||
|
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @dark {
|
||||||
|
.ant-checkbox-indeterminate {
|
||||||
|
.ant-checkbox-inner {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-header {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-small {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-bordered {
|
||||||
|
border: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-notice-wrapper .ant-tabs-content {
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-notice-wrapper .ant-tabs-tabpane-active {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-footer {
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
1499
nginx-ui-frontend/src/assets/css/style.bak.less
Normal file
1499
nginx-ui-frontend/src/assets/css/style.bak.less
Normal file
File diff suppressed because it is too large
Load diff
150
nginx-ui-frontend/src/assets/css/style.less
Normal file
150
nginx-ui-frontend/src/assets/css/style.less
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/* 基准 */
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
//cursor: default;
|
||||||
|
//-webkit-user-select: none;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-family: "Myriad Pro", "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #5aaafc;
|
||||||
|
//color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: #5aaafc;
|
||||||
|
//color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #444;
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
text-decoration: none;
|
||||||
|
-webkit-transition: all .3s ease;
|
||||||
|
-moz-transition: all .3s ease;
|
||||||
|
-ms-transition: all .3s ease;
|
||||||
|
-o-transition: all .3s ease;
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #19B5FE;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: normal
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-style: normal
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 400
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: Menlo, monospace;
|
||||||
|
font-size: 90%
|
||||||
|
}
|
||||||
|
|
||||||
|
p > code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
background: #e1e9ed
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
text-align: left;
|
||||||
|
overflow-x: scroll;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10pt 15pt;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-shadow: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-max {
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification {
|
||||||
|
z-index: 2100
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-comment-actions {
|
||||||
|
margin-top: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input:hover {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
z-index: 2030 !important;
|
||||||
|
background: #1890ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grecaptcha-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.std-border-radius {
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
box-shadow: 0 0 30px rgba(200, 200, 200, 0.25);
|
||||||
|
}
|
BIN
nginx-ui-frontend/src/assets/img/logo.png
Normal file
BIN
nginx-ui-frontend/src/assets/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
46
nginx-ui-frontend/src/components/Breadcrumb/Breadcrumb.vue
Normal file
46
nginx-ui-frontend/src/components/Breadcrumb/Breadcrumb.vue
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
|
||||||
|
<router-link
|
||||||
|
v-if="item.name != name && index != 1"
|
||||||
|
:to="{ path: item.path === '' ? '/' : item.path }"
|
||||||
|
>{{ item.name }}
|
||||||
|
</router-link>
|
||||||
|
<span v-else>{{ item.name }}</span>
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
breadList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBreadcrumb() {
|
||||||
|
this.breadList = []
|
||||||
|
//this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: '首页'}})
|
||||||
|
|
||||||
|
this.name = this.$route.name
|
||||||
|
this.$route.matched.forEach(item => {
|
||||||
|
//item.name !== 'index' && this.breadList.push(item)
|
||||||
|
this.breadList.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="ant-pro-footer-toolbar">
|
||||||
|
<div style="float: left">
|
||||||
|
<slot name="extra">{{ extra }}</slot>
|
||||||
|
</div>
|
||||||
|
<div style="float: right">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FooterToolBar',
|
||||||
|
props: {
|
||||||
|
prefixCls: {
|
||||||
|
type: String,
|
||||||
|
default: 'ant-pro-footer-toolbar'
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.ant-pro-footer-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 56px;
|
||||||
|
line-height: 56px;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||||
|
background: #ffffff8c;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: rgba(24, 24, 24, 0.62);
|
||||||
|
border-top: unset;
|
||||||
|
}
|
||||||
|
padding: 0 24px;
|
||||||
|
z-index: 9;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
3
nginx-ui-frontend/src/components/FooterToolbar/index.js
Normal file
3
nginx-ui-frontend/src/components/FooterToolbar/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import FooterToolBar from './FooterToolBar'
|
||||||
|
|
||||||
|
export default FooterToolBar
|
45
nginx-ui-frontend/src/components/Logo/Logo.vue
Normal file
45
nginx-ui-frontend/src/components/Logo/Logo.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="logo">
|
||||||
|
<img :src="logo"/>
|
||||||
|
<div class="text">Nginx UI</div>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Logo',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logo: require('@/assets/img/logo.png')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.logo {
|
||||||
|
margin: 8px 18px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
transition: all 0.3s;
|
||||||
|
height: 56px;
|
||||||
|
width: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 46px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
float: left;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
206
nginx-ui-frontend/src/components/PageHeader/PageHeader.vue
Normal file
206
nginx-ui-frontend/src/components/PageHeader/PageHeader.vue
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="!$route.meta.hiddenHeaderContent" class="page-header">
|
||||||
|
<div class="page-header-index-wide">
|
||||||
|
<s-breadcrumb/>
|
||||||
|
<div class="detail">
|
||||||
|
<div class="main">
|
||||||
|
<div class="row">
|
||||||
|
<img v-if="logo" :src="logo" class="logo"/>
|
||||||
|
<h1 v-if="title" class="title">{{ title }}</h1>
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div v-if="avatar" class="avatar">
|
||||||
|
<a-avatar :src="avatar"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="this.$slots.content" class="headerContent">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="this.$slots.extra" class="extra">
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="pageMenu"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageHeader',
|
||||||
|
components: {
|
||||||
|
's-breadcrumb': Breadcrumb
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: true,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 32px 0;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
/*margin-bottom: 16px;*/
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex: 0 1 72px;
|
||||||
|
margin: 0 24px 8px 0;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
border-radius: 72px;
|
||||||
|
display: block;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 28px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content,
|
||||||
|
.headerContent {
|
||||||
|
flex: auto;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
margin-left: 88px;
|
||||||
|
min-width: 242px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
margin-left: 56px;
|
||||||
|
min-width: 266px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .page-header {
|
||||||
|
.main {
|
||||||
|
.row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex: 0 1 25%;
|
||||||
|
margin: 0 2% 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content,
|
||||||
|
.headerContent {
|
||||||
|
flex: 0 1 70%;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
margin-left: unset;
|
||||||
|
min-width: 266px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
3
nginx-ui-frontend/src/components/PageHeader/index.js
Normal file
3
nginx-ui-frontend/src/components/PageHeader/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import PageHeader from './PageHeader'
|
||||||
|
|
||||||
|
export default PageHeader
|
129
nginx-ui-frontend/src/components/StdDataDisplay/StdCurd.vue
Normal file
129
nginx-ui-frontend/src/components/StdDataDisplay/StdCurd.vue
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-card :title="title">
|
||||||
|
<a v-if="!disable_add" slot="extra" @click="add">添加</a>
|
||||||
|
<std-table
|
||||||
|
ref="table"
|
||||||
|
:api="api"
|
||||||
|
:columns="columns"
|
||||||
|
:data_key="data_key"
|
||||||
|
:deletable="deletable"
|
||||||
|
:disable_search="disable_search"
|
||||||
|
:edit_text="edit_text"
|
||||||
|
:soft_delete="soft_delete"
|
||||||
|
@clickEdit="edit"
|
||||||
|
/>
|
||||||
|
</a-card>
|
||||||
|
<a-modal
|
||||||
|
:mask="false"
|
||||||
|
:title="data.id ? '编辑 ID: ' + data.id : '添加'"
|
||||||
|
:visible="visible"
|
||||||
|
cancel-text="取消"
|
||||||
|
ok-text="保存"
|
||||||
|
@cancel="visible=false;error={}"
|
||||||
|
@ok="ok"
|
||||||
|
>
|
||||||
|
<std-data-entry ref="std_data_entry" :data-list="editableColumns(columns)" :data-source="data"
|
||||||
|
:error="error"/>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdTable from './StdTable'
|
||||||
|
import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdCurd',
|
||||||
|
components: {
|
||||||
|
StdTable,
|
||||||
|
StdDataEntry
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '列表'
|
||||||
|
},
|
||||||
|
api: Object,
|
||||||
|
columns: Array,
|
||||||
|
data_key: {
|
||||||
|
type: String,
|
||||||
|
default: 'data'
|
||||||
|
},
|
||||||
|
disable_search: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disable_add: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
soft_delete: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
edit_text: String,
|
||||||
|
deletable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
data: {
|
||||||
|
id: null,
|
||||||
|
},
|
||||||
|
error: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editableColumns(columns) {
|
||||||
|
return columns.filter((c) => {
|
||||||
|
return c.edit
|
||||||
|
})
|
||||||
|
},
|
||||||
|
uploadColumns(columns) {
|
||||||
|
return columns.filter((c) => {
|
||||||
|
return c.edit && c.edit.type === 'upload'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
add() {
|
||||||
|
this.visible = true
|
||||||
|
this.data = {
|
||||||
|
id: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.api.save((this.data.id ? this.data.id : null), this.data).then(r => {
|
||||||
|
this.$message.success('保存成功')
|
||||||
|
const refs = this.$refs.std_data_entry.$refs
|
||||||
|
this.uploadColumns(this.columns).forEach(c => {
|
||||||
|
const t = refs['std_upload_' + c.dataIndex][0]
|
||||||
|
if (t) {
|
||||||
|
t.upload()
|
||||||
|
}
|
||||||
|
delete r[c.dataIndex]
|
||||||
|
})
|
||||||
|
this.data = this.extend(this.data, r)
|
||||||
|
this.$refs.table.get_list()
|
||||||
|
}).catch(error => {
|
||||||
|
this.$message.error('保存失败')
|
||||||
|
this.error = error.errors
|
||||||
|
})
|
||||||
|
},
|
||||||
|
edit(id) {
|
||||||
|
this.api.get(id).then(r => {
|
||||||
|
this.data = r
|
||||||
|
this.visible = true
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.error('服务器错误')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="Object.keys(pagination).length !== 0">
|
||||||
|
<a-pagination
|
||||||
|
:current="pagination.current_page"
|
||||||
|
:hideOnSinglePage="true"
|
||||||
|
:pageSize="pagination.per_page"
|
||||||
|
:size="size"
|
||||||
|
:total="pagination.total"
|
||||||
|
class="pagination"
|
||||||
|
@change="changePage"
|
||||||
|
/>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'StdPagination',
|
||||||
|
props: {
|
||||||
|
pagination: Object,
|
||||||
|
size: {
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changePage(num) {
|
||||||
|
return this.$emit('changePage', num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination {
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
271
nginx-ui-frontend/src/components/StdDataDisplay/StdTable.vue
Normal file
271
nginx-ui-frontend/src/components/StdDataDisplay/StdTable.vue
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
<template>
|
||||||
|
<div class="std-table">
|
||||||
|
<a-form v-if="!disable_search" layout="inline">
|
||||||
|
<a-form-item
|
||||||
|
v-for="c in searchColumns(columns)" :key="c.dataIndex?c.dataIndex:c.name"
|
||||||
|
:label="c.title">
|
||||||
|
<a-input v-if="c.search.type==='input'" v-model="params[c.dataIndex]"/>
|
||||||
|
<a-checkbox
|
||||||
|
v-if="c.search.type==='checkbox'"
|
||||||
|
:default-checked="c.search.default"
|
||||||
|
:name="c.search.condition?c.search.condition:c.dataIndex"
|
||||||
|
@change="checked"/>
|
||||||
|
<a-slider
|
||||||
|
v-else-if="c.search.type==='slider'"
|
||||||
|
v-model="params[c.dataIndex]"
|
||||||
|
:marks="c.mask"
|
||||||
|
:max="c.search.max"
|
||||||
|
:min="c.search.min"
|
||||||
|
style="width: 130px"
|
||||||
|
/>
|
||||||
|
<a-select v-if="c.search.type==='select'" v-model="params[c.dataIndex]"
|
||||||
|
style="width: 130px">
|
||||||
|
<a-select-option v-for="(v,k) in c.mask" :key="k" :value="k">{{ v }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{span:8}">
|
||||||
|
<a-button type="primary" @click="get_list()">查询</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{span:8}">
|
||||||
|
<a-button @click="reset_search">重置</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<div v-if="soft_delete" style="text-align: right">
|
||||||
|
<a v-if="params['trashed']" href="javascript:;"
|
||||||
|
@click="params['trashed']=false; get_list()">返回</a>
|
||||||
|
<a v-else href="javascript:;" @click="params['trashed']=true; get_list()">回收站</a>
|
||||||
|
</div>
|
||||||
|
<a-table
|
||||||
|
:columns="pithyColumns(columns)"
|
||||||
|
:customRow="row"
|
||||||
|
:data-source="data_source"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
:row-key="'name'"
|
||||||
|
:rowSelection="{
|
||||||
|
selectedRowKeys: selectedRowKeys,
|
||||||
|
onChange: onSelectChange,
|
||||||
|
onSelect: onSelect,
|
||||||
|
type: selectionType,
|
||||||
|
}"
|
||||||
|
@change="stdChange"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="c in pithyColumns(columns)"
|
||||||
|
:slot="c.scopedSlots.customRender"
|
||||||
|
slot-scope="text, record">
|
||||||
|
<span v-if="c.badge" :key="c.dataIndex">
|
||||||
|
<a-badge v-if="text === true || text > 0" status="success"/>
|
||||||
|
<a-badge v-else status="error"/>
|
||||||
|
{{ c.mask ? c.mask[text] : text }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="c.datetime" :key="c.dataIndex">{{ moment(text).format("yyyy-MM-DD HH:mm:ss") }}</span>
|
||||||
|
<span v-else-if="c.click" :key="c.dataIndex">
|
||||||
|
<a href="javascript:;" @click="handleClick(record[c.click.index?c.click.index:c.dataIndex],
|
||||||
|
c.click.index?c.click.index:c.dataIndex,
|
||||||
|
c.click.method, c.click.path)">
|
||||||
|
{{ text != null ? text : c.default }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span v-else :key="c.dataIndex">
|
||||||
|
{{ text != null ? (c.mask ? c.mask[text] : text) : c.default }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-if="!pithy" slot="action" slot-scope="text, record">
|
||||||
|
<slot name="action" :record="record" />
|
||||||
|
<a href="javascript:;" @click="$emit('clickEdit', record)">
|
||||||
|
<template v-if="edit_text">{{ edit_text }}</template>
|
||||||
|
<template v-else>编辑</template>
|
||||||
|
</a>
|
||||||
|
<template v-if="deletable">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="soft_delete&¶ms.trashed"
|
||||||
|
cancelText="再想想"
|
||||||
|
okText="是的" title="你确定要反删除?"
|
||||||
|
@confirm="restore(record.name)">
|
||||||
|
<a href="javascript:;">反删除</a>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm
|
||||||
|
v-else
|
||||||
|
cancelText="再想想"
|
||||||
|
okText="是的" title="你确定要删除?"
|
||||||
|
@confirm="destroy(record.name)"
|
||||||
|
>
|
||||||
|
<a href="javascript:;">删除</a>
|
||||||
|
</a-popconfirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</a-table>
|
||||||
|
<std-pagination :pagination="pagination" @changePage="get_list"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdPagination from './StdPagination'
|
||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdTable',
|
||||||
|
components: {
|
||||||
|
StdPagination,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
columns: Array,
|
||||||
|
api: Object,
|
||||||
|
data_key: String,
|
||||||
|
selectionType: {
|
||||||
|
type: String,
|
||||||
|
default: 'checkbox',
|
||||||
|
validator: function (value) {
|
||||||
|
return ['checkbox', 'radio'].indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pithy: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disable_search: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
soft_delete: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
edit_text: String,
|
||||||
|
deletable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
moment,
|
||||||
|
data_source: [],
|
||||||
|
loading: true,
|
||||||
|
pagination: {
|
||||||
|
total: 1,
|
||||||
|
per_page: 10,
|
||||||
|
current_page: 1,
|
||||||
|
total_pages: 1
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
|
selectedRowKeys: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.get_list()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
get_list(page_num = null) {
|
||||||
|
this.loading = true
|
||||||
|
this.params['page'] = page_num
|
||||||
|
this.api.get_list(this.params).then(response => {
|
||||||
|
if (response[this.data_key] === undefined && response.data !== undefined) {
|
||||||
|
this.data_source = response.data
|
||||||
|
} else {
|
||||||
|
this.data_source = response[this.data_key]
|
||||||
|
}
|
||||||
|
if (response.pagination !== undefined) {
|
||||||
|
this.pagination = response.pagination
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
console.log(e)
|
||||||
|
this.$message.error('服务器错误')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stdChange(pagination, filters, sorter) {
|
||||||
|
if (sorter) {
|
||||||
|
this.params['order_by'] = sorter.field
|
||||||
|
this.params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.get_list()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy(id) {
|
||||||
|
this.api.destroy(id).then(() => {
|
||||||
|
this.get_list()
|
||||||
|
this.$message.success('删除 ID: ' + id + ' 成功')
|
||||||
|
}).catch(e => {
|
||||||
|
this.$message.error('服务器错误' + (e.message ? " " + e.message : ""))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
restore(id) {
|
||||||
|
this.api.restore(id).then(() => {
|
||||||
|
this.get_list()
|
||||||
|
this.$message.success('反删除 ID: ' + id + ' 成功')
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.error('服务器错误')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
searchColumns(columns) {
|
||||||
|
return columns.filter((column) => {
|
||||||
|
return column.search
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pithyColumns(columns) {
|
||||||
|
if (this.pithy) {
|
||||||
|
return columns.filter((c) => {
|
||||||
|
return c.pithy === true && c.display !== false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return columns.filter((c) => {
|
||||||
|
return c.display !== false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checked(c) {
|
||||||
|
this.params[c.target.value] = c.target.checked
|
||||||
|
},
|
||||||
|
onSelectChange(selectedRowKeys) {
|
||||||
|
this.selectedRowKeys = selectedRowKeys
|
||||||
|
this.$emit('selected', selectedRowKeys)
|
||||||
|
},
|
||||||
|
onSelect(record) {
|
||||||
|
console.log(record)
|
||||||
|
this.$emit('selectedRecord', record)
|
||||||
|
},
|
||||||
|
handleClick(data, index, method = '', path = '') {
|
||||||
|
if (method === 'router') {
|
||||||
|
this.$router.push(path + '/' + data).then()
|
||||||
|
} else {
|
||||||
|
this.params[index] = data
|
||||||
|
this.get_list()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
row(record) {
|
||||||
|
return {
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
this.$emit('clickRow', record.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reset_search() {
|
||||||
|
this.params = {}
|
||||||
|
await this.get_list()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.ant-form {
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider {
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.std-table {
|
||||||
|
.ant-table-wrapper {
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
140
nginx-ui-frontend/src/components/StdDataEntry/StdDataEntry.vue
Normal file
140
nginx-ui-frontend/src/components/StdDataEntry/StdDataEntry.vue
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<a-form :layout="layout" class="std-data-entry">
|
||||||
|
<a-form-item
|
||||||
|
v-for="d in M_dataList" :key="d.dataIndex" :help="error[d.dataIndex] ? error[d.dataIndex].toString() : null"
|
||||||
|
:label="d.title"
|
||||||
|
:labelCol="d.edit.labelCol"
|
||||||
|
:validate-status="error[d.dataIndex] ? 'error' :'success'"
|
||||||
|
:wrapperCol="d.edit.wrapperCol"
|
||||||
|
>
|
||||||
|
<a-input v-if="d.edit.type==='input'" v-model="dataSource[d.dataIndex]" :placeholder="d.edit.placeholder"/>
|
||||||
|
<a-textarea v-else-if="d.edit.type==='textarea'" v-model="dataSource[d.dataIndex]"
|
||||||
|
:rows="d.edit.row?d.edit.row:5"/>
|
||||||
|
<std-select-option
|
||||||
|
v-else-if="d.edit.type==='select'"
|
||||||
|
v-model="temp[d.dataIndex]"
|
||||||
|
:options="d.mask"
|
||||||
|
:key-type="d.edit.key_type ? d.edit.key_type : 'int'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<std-selector
|
||||||
|
v-else-if="d.edit.type==='selector'" v-model="temp[d.dataIndex]" :api="d.edit.api"
|
||||||
|
:columns="d.edit.columns"
|
||||||
|
:data_key="d.edit.data_key"
|
||||||
|
:disable_search="d.edit.disable_search" :pagination_method="d.edit.pagination_method"
|
||||||
|
:record-value-index="d.edit.recordValueIndex" :value="temp[d.edit.valueIndex]"
|
||||||
|
selection-type="radio"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-input-number v-else-if="d.edit.type==='number'" v-model="temp[d.dataIndex]"
|
||||||
|
:min="d.edit.min" :step="d.edit.step"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<std-date-picker v-else-if="d.edit.type==='date_picker'" v-model="temp[d.dataIndex]"
|
||||||
|
:show-time="d.edit.showTime"/>
|
||||||
|
|
||||||
|
<a-slider
|
||||||
|
v-else-if="d.edit.type==='slider'"
|
||||||
|
v-model="temp[d.dataIndex]"
|
||||||
|
:marks="d.mask"
|
||||||
|
:max="d.edit.max"
|
||||||
|
:min="d.edit.min"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-switch
|
||||||
|
v-else-if="d.edit.type==='switch'"
|
||||||
|
v-model="temp[d.dataIndex]"
|
||||||
|
@change="$emit(d.edit.event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<std-transfer
|
||||||
|
v-else-if="d.edit.type==='transfer'"
|
||||||
|
v-model="temp[d.dataIndex]"
|
||||||
|
:api="d.edit.api"
|
||||||
|
:data-key="d.edit.dataKey"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-else-if="d.edit.type==='readonly'">
|
||||||
|
{{ d.mask ? d.mask[temp[d.dataIndex]] : temp[d.dataIndex] }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<slot name="action"/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdSelectOption from './StdSelectOption'
|
||||||
|
import StdSelector from './StdSelector'
|
||||||
|
import StdDatePicker from './StdDatePicker'
|
||||||
|
import StdTransfer from './StdTransfer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdDataEntry',
|
||||||
|
components: {
|
||||||
|
StdTransfer,
|
||||||
|
StdDatePicker,
|
||||||
|
StdSelectOption,
|
||||||
|
StdSelector
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
dataList: [Array, Object],
|
||||||
|
dataSource: Object,
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
default: 'vertical',
|
||||||
|
validator: value => {
|
||||||
|
return ['horizontal', 'vertical', 'inline'].indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'dataSource',
|
||||||
|
event: 'changeDataSource'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
temp: null,
|
||||||
|
i: 0,
|
||||||
|
M_dataList: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dataSource() {
|
||||||
|
this.temp = this.dataSource
|
||||||
|
},
|
||||||
|
dataList() {
|
||||||
|
this.M_dataList = this.editableColumns(this.dataList)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.temp = this.dataSource
|
||||||
|
if (this.layout === 'horizontal') {
|
||||||
|
this.labelCol = {span: 4}
|
||||||
|
this.wrapperCol = {span: 18}
|
||||||
|
}
|
||||||
|
this.M_dataList = this.editableColumns(this.dataList)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editableColumns(columns) {
|
||||||
|
if (typeof columns === 'object') {
|
||||||
|
columns = Object.values(columns)
|
||||||
|
}
|
||||||
|
return columns.filter((c) => {
|
||||||
|
return c.edit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<a-date-picker
|
||||||
|
v-model="dateModel"
|
||||||
|
:show-time="showTime"
|
||||||
|
@change="(r, d) => {changeDate(d)}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const moment = require('moment')
|
||||||
|
moment.locale('zh-cn')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdDatePicker',
|
||||||
|
props: {
|
||||||
|
date: String,
|
||||||
|
showTime: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'date',
|
||||||
|
event: 'changeDate'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
moment,
|
||||||
|
dateModel: this.date ? moment(this.date) : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
date() {
|
||||||
|
this.dateModel = this.date ? moment(this.date) : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeDate(d) {
|
||||||
|
this.$emit('changeDate', d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model="tempValue"
|
||||||
|
:defaultValue="Object.keys(options)[0]"
|
||||||
|
@change="$emit('change', isNaN(parseInt(tempValue)) || keyType === 'string' ? tempValue : parseInt(tempValue) )">
|
||||||
|
<a-select-option v-for="(v,k) in options" :key="k">{{ v }}</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'StdSelectOption',
|
||||||
|
props: {
|
||||||
|
value: [Number, String],
|
||||||
|
options: [Array, Object],
|
||||||
|
keyType: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'int'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'value',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tempValue: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value() {
|
||||||
|
this.tempValue = this.value != null ? this.value.toString() : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.tempValue = this.value != null ? this.value.toString() : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.ant-select {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
</style>
|
104
nginx-ui-frontend/src/components/StdDataEntry/StdSelector.vue
Normal file
104
nginx-ui-frontend/src/components/StdDataEntry/StdSelector.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="std-selector">
|
||||||
|
<a-input v-model="_key" disabled hidden/>
|
||||||
|
<a-input v-model="M_value" disabled/>
|
||||||
|
<a-button @click="visible=true">更变</a-button>
|
||||||
|
<a-modal
|
||||||
|
:mask="false"
|
||||||
|
:visible="visible"
|
||||||
|
cancel-text="取消"
|
||||||
|
ok-text="选择"
|
||||||
|
title="选择器"
|
||||||
|
@cancel="visible=false"
|
||||||
|
@ok="ok()"
|
||||||
|
>
|
||||||
|
<std-table
|
||||||
|
:api="api"
|
||||||
|
:columns="columns"
|
||||||
|
:data_key="data_key"
|
||||||
|
:disable_search="disable_search"
|
||||||
|
:pagination_method="pagination_method"
|
||||||
|
:pithy="true"
|
||||||
|
:selectionType="selectionType"
|
||||||
|
@selected="onSelect"
|
||||||
|
@selectedRecord="r => {record = r}"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdTable from '@/components/StdDataDisplay/StdTable'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdSelector',
|
||||||
|
components: {StdTable},
|
||||||
|
props: {
|
||||||
|
_key: [Number, String],
|
||||||
|
value: String,
|
||||||
|
recordValueIndex: [Number, String],
|
||||||
|
selectionType: {
|
||||||
|
type: String,
|
||||||
|
default: 'checkbox',
|
||||||
|
validator: function (value) {
|
||||||
|
return ['checkbox', 'radio'].indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
api: Object,
|
||||||
|
columns: Array,
|
||||||
|
data_key: String,
|
||||||
|
pagination_method: {
|
||||||
|
type: String,
|
||||||
|
validator: function (value) {
|
||||||
|
return ['a', 'b'].indexOf(value) !== -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disable_search: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: '_key',
|
||||||
|
event: 'changeSelect'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
selected: [],
|
||||||
|
record: {},
|
||||||
|
M_value: this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value() {
|
||||||
|
this.M_value = this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSelect(selected) {
|
||||||
|
this.selected = selected
|
||||||
|
},
|
||||||
|
ok() {
|
||||||
|
this.visible = false
|
||||||
|
let selected = this.selected
|
||||||
|
if (this.selectionType === 'radio') {
|
||||||
|
selected = this.selected[0]
|
||||||
|
}
|
||||||
|
this.M_value = this.record[this.recordValueIndex]
|
||||||
|
this.$emit('changeSelect', selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.std-selector {
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
width: auto;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<a-transfer
|
||||||
|
:data-source="dataSource"
|
||||||
|
:render="item=>item.title"
|
||||||
|
:selectedKeys="selectedKeys"
|
||||||
|
:targetKeys="targetKeys"
|
||||||
|
:titles="['可添加', '已添加']"
|
||||||
|
@change="handleChange"
|
||||||
|
@selectChange="handleSelectChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const mockData = []
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
mockData.push({
|
||||||
|
key: i.toString(),
|
||||||
|
title: `content${i + 1}`,
|
||||||
|
description: `description of content${i + 1}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
name: 'StdTransfer',
|
||||||
|
props: {
|
||||||
|
api: Function,
|
||||||
|
dataKey: String,
|
||||||
|
target: String
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'target',
|
||||||
|
event: 'changeTarget'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
targetKeys: [],
|
||||||
|
selectedKeys: [],
|
||||||
|
dataSource: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.targetKeys = this.target.split(',')
|
||||||
|
if (this.api) {
|
||||||
|
this.api().then(r => {
|
||||||
|
const dataSource = []
|
||||||
|
r[this.dataKey ? this.dataKey : 'data'].forEach(v => {
|
||||||
|
dataSource.push({
|
||||||
|
key: v.id.toString(),
|
||||||
|
title: `${v.title}`,
|
||||||
|
description: `${v.description}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.dataSource = dataSource
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
targetKeys() {
|
||||||
|
this.$emit('changeTarget', this.targetKeys.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleChange(nextTargetKeys) {
|
||||||
|
this.targetKeys = nextTargetKeys
|
||||||
|
},
|
||||||
|
handleSelectChange(sourceSelectedKeys, targetSelectedKeys) {
|
||||||
|
this.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
78
nginx-ui-frontend/src/components/StdFormCard/StdFormCard.vue
Normal file
78
nginx-ui-frontend/src/components/StdFormCard/StdFormCard.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="std-form-card">
|
||||||
|
<a-row
|
||||||
|
:align="'middle'"
|
||||||
|
:type="'flex'"
|
||||||
|
class="container"
|
||||||
|
>
|
||||||
|
<a-col
|
||||||
|
:lg="8"
|
||||||
|
:md="10"
|
||||||
|
:sm="12"
|
||||||
|
:xl="6"
|
||||||
|
:xs="24"
|
||||||
|
:xxl="5"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<std-form-card-content
|
||||||
|
:error="error" :options="options"
|
||||||
|
@onSubmit="value => {$emit('onSubmit', value)}"/>
|
||||||
|
<vue-particles
|
||||||
|
:click-effect="false"
|
||||||
|
:hover-effect="false"
|
||||||
|
:move-speed="3"
|
||||||
|
:particlesNumber="60"
|
||||||
|
class="particles"
|
||||||
|
color="#dedede"/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VueParticles from 'vue-particles'
|
||||||
|
import StdFormCardContent from '@/components/StdFormCard/StdFormCardContent'
|
||||||
|
|
||||||
|
Vue.use(VueParticles)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdFormCard',
|
||||||
|
components: {
|
||||||
|
StdFormCardContent
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
options: Object,
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.particles {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<a-card :bordered="false" class="form-card">
|
||||||
|
<a-form :form="form" @submit="handleSubmit">
|
||||||
|
<div class="logo">
|
||||||
|
<img :src="logo"/>
|
||||||
|
</div>
|
||||||
|
<p class="title">
|
||||||
|
{{ options.title }}
|
||||||
|
</p>
|
||||||
|
<a-form-item
|
||||||
|
v-for="item in options.items"
|
||||||
|
:key="item.label"
|
||||||
|
:help="errors[item.decorator[0]] ? errors[item.decorator[0]] : null"
|
||||||
|
:label="item.label"
|
||||||
|
:validate-status="errors[item.decorator[0]] ? 'error' :'success'"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-decorator="item.decorator"
|
||||||
|
:autocomplate="item.autocomplate ? 'on' : 'off'"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
:type="item.type"
|
||||||
|
>
|
||||||
|
<a-icon slot="prefix" :type="item.icon" style="color: rgba(0,0,0,.25)"/>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="action">
|
||||||
|
<div class="center">
|
||||||
|
<a-button
|
||||||
|
:loading="loading"
|
||||||
|
class="std-border-radius"
|
||||||
|
html-type="submit"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
{{ options.button_text }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="small-link center">
|
||||||
|
<slot name="small-link"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//import {VueReCaptcha} from 'vue-recaptcha-v3'
|
||||||
|
//import Vue from 'vue'
|
||||||
|
|
||||||
|
/*Vue.use(VueReCaptcha, {
|
||||||
|
siteKey: process.env.VUE_APP_RECAPTCHA_SITEKEY,
|
||||||
|
loaderOptions: {
|
||||||
|
useRecaptchaNet: true
|
||||||
|
}
|
||||||
|
})*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StdFormCardContent',
|
||||||
|
props: {
|
||||||
|
options: Object,
|
||||||
|
errors: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logo: require('@/assets/img/logo.png'),
|
||||||
|
loading: false,
|
||||||
|
form: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.form = this.$form.createForm(this)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (!err) {
|
||||||
|
this.loading = true
|
||||||
|
//this.$recaptchaLoaded().then(() => {
|
||||||
|
//this.$recaptcha('std_form').then(token => {
|
||||||
|
//values.token = token
|
||||||
|
this.$emit('onSubmit', values)
|
||||||
|
//})
|
||||||
|
// })
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.form-card {
|
||||||
|
.ant-form-item {
|
||||||
|
input {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-card {
|
||||||
|
box-shadow: 0 0 30px rgba(200, 200, 200, 0.25);
|
||||||
|
|
||||||
|
.ant-form {
|
||||||
|
max-width: 250px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 17px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: grid;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
.small-link {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 15px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
142
nginx-ui-frontend/src/layouts/BaseLayout.vue
Normal file
142
nginx-ui-frontend/src/layouts/BaseLayout.vue
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<a-config-provider :locale="zh_CN">
|
||||||
|
<a-layout style="min-height: 100%;">
|
||||||
|
<a-drawer
|
||||||
|
v-show="clientWidth<512"
|
||||||
|
:closable="false"
|
||||||
|
:visible="collapsed"
|
||||||
|
placement="left"
|
||||||
|
@close="collapsed=false"
|
||||||
|
>
|
||||||
|
<side-bar/>
|
||||||
|
</a-drawer>
|
||||||
|
|
||||||
|
<a-layout-sider
|
||||||
|
v-show="clientWidth>=512"
|
||||||
|
v-model="collapsed"
|
||||||
|
:collapsible="true"
|
||||||
|
:style="{zIndex: 11}"
|
||||||
|
theme="light"
|
||||||
|
>
|
||||||
|
<side-bar/>
|
||||||
|
</a-layout-sider>
|
||||||
|
|
||||||
|
<a-layout>
|
||||||
|
<a-layout-header :style="{position: 'fixed', zIndex: 10, width:'100%'}">
|
||||||
|
<header-layout @clickUnFold="collapsed=true"/>
|
||||||
|
</a-layout-header>
|
||||||
|
|
||||||
|
<a-layout-content>
|
||||||
|
<page-header :title="$route.name"/>
|
||||||
|
<div class="router-view">
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</a-layout-content>
|
||||||
|
|
||||||
|
<a-layout-footer>
|
||||||
|
<footer-layout/>
|
||||||
|
</a-layout-footer>
|
||||||
|
</a-layout>
|
||||||
|
|
||||||
|
</a-layout>
|
||||||
|
</a-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderLayout from './HeaderLayout'
|
||||||
|
import SideBar from './SideBar'
|
||||||
|
import FooterLayout from './FooterLayout'
|
||||||
|
import PageHeader from '@/components/PageHeader/PageHeader'
|
||||||
|
import zh_CN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BaseLayout',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collapsed: this.collapse(),
|
||||||
|
zh_CN,
|
||||||
|
clientWidth: document.body.clientWidth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.onresize = () => {
|
||||||
|
this.collapsed = this.collapse()
|
||||||
|
this.clientWidth = this.getClientWidth()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SideBar,
|
||||||
|
PageHeader,
|
||||||
|
HeaderLayout,
|
||||||
|
FooterLayout
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@dark: ~"(prefers-color-scheme: dark)";
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider {
|
||||||
|
background-color: #ffffff;
|
||||||
|
@media @dark {
|
||||||
|
background-color: #28292c;
|
||||||
|
}
|
||||||
|
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @dark {
|
||||||
|
.ant-checkbox-indeterminate {
|
||||||
|
.ant-checkbox-inner {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-header {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-small {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-bordered {
|
||||||
|
border: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-notice-wrapper .ant-tabs-content {
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-notice-wrapper .ant-tabs-tabpane-active {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-footer {
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-content {
|
||||||
|
margin: 64px 0;
|
||||||
|
min-height: auto;
|
||||||
|
|
||||||
|
.router-view {
|
||||||
|
padding: 20px;
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
13
nginx-ui-frontend/src/layouts/BaseRouterView.vue
Normal file
13
nginx-ui-frontend/src/layouts/BaseRouterView.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'BaseRouterView'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
22
nginx-ui-frontend/src/layouts/FooterLayout.vue
Normal file
22
nginx-ui-frontend/src/layouts/FooterLayout.vue
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div class="footer center">
|
||||||
|
Copyright © 2020 - {{ thisYear }} 0xJacky
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FooterComponent',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
thisYear: new Date().getFullYear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
50
nginx-ui-frontend/src/layouts/HeaderLayout.vue
Normal file
50
nginx-ui-frontend/src/layouts/HeaderLayout.vue
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="header">
|
||||||
|
<div class="tool">
|
||||||
|
<a-icon type="menu-unfold" @click="$emit('clickUnFold')"/>
|
||||||
|
</div>
|
||||||
|
<div class="user-wrapper">
|
||||||
|
<a href="/index.html">
|
||||||
|
<a-icon type="home"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeaderComponent',
|
||||||
|
components: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 20px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: #28292c;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
border-bottom: unset;
|
||||||
|
}
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool {
|
||||||
|
position: fixed;
|
||||||
|
left: 20px;
|
||||||
|
@media (min-width: 512px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
33
nginx-ui-frontend/src/layouts/Loading.vue
Normal file
33
nginx-ui-frontend/src/layouts/Loading.vue
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="loading"
|
||||||
|
class="loading"
|
||||||
|
>
|
||||||
|
<div class="wrapper center">
|
||||||
|
<a-spin>
|
||||||
|
<a-icon
|
||||||
|
slot="indicator"
|
||||||
|
spin
|
||||||
|
style="font-size: 30px"
|
||||||
|
type="loading"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Loading',
|
||||||
|
props: {
|
||||||
|
loading: {
|
||||||
|
type: [Boolean, String],
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
81
nginx-ui-frontend/src/layouts/SideBar.vue
Normal file
81
nginx-ui-frontend/src/layouts/SideBar.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="sidebar">
|
||||||
|
<logo/>
|
||||||
|
<a-menu
|
||||||
|
:openKeys="openKeys"
|
||||||
|
mode="inline"
|
||||||
|
@openChange="onOpenChange"
|
||||||
|
>
|
||||||
|
<template v-for="sidebar in visible(sidebars)">
|
||||||
|
<a-menu-item v-if="!sidebar.children" :key="sidebar.path"
|
||||||
|
@click="$router.push('/'+sidebar.path).catch(() => {})">
|
||||||
|
<a-icon :type="sidebar.meta.icon"/>
|
||||||
|
<span>{{ sidebar.name }}</span>
|
||||||
|
</a-menu-item>
|
||||||
|
|
||||||
|
<a-sub-menu v-else :key="sidebar.path">
|
||||||
|
<span slot="title"><a-icon :type="sidebar.meta.icon"/><span>{{ sidebar.name }}</span></span>
|
||||||
|
<a-menu-item v-for="child in visible(sidebar.children)" :key="child.name">
|
||||||
|
<router-link :to="'/'+sidebar.path+'/'+child.path">
|
||||||
|
{{ child.name }}
|
||||||
|
</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
</template>
|
||||||
|
</a-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Logo from '@/components/Logo/Logo'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SideBar',
|
||||||
|
components: {Logo},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rootSubmenuKeys: [],
|
||||||
|
openKeys: [],
|
||||||
|
sidebars: this.$routeConfig[0]['children']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.sidebars.forEach((element) => {
|
||||||
|
this.rootSubmenuKeys.push(element)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpenChange(openKeys) {
|
||||||
|
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
|
||||||
|
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
|
||||||
|
this.openKeys = openKeys
|
||||||
|
} else {
|
||||||
|
this.openKeys = latestOpenKey ? [latestOpenKey] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visible(sidebars) {
|
||||||
|
return sidebars.filter(c => {
|
||||||
|
return c.meta === undefined || (c.meta.hiddenInSidebar === undefined || c.meta.hiddenInSidebar !== true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider-collapsed .logo {
|
||||||
|
width: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
|
||||||
|
border-right: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
105
nginx-ui-frontend/src/lazy.js
Normal file
105
nginx-ui-frontend/src/lazy.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Col,
|
||||||
|
Collapse,
|
||||||
|
Comment,
|
||||||
|
ConfigProvider,
|
||||||
|
DatePicker,
|
||||||
|
Descriptions,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Dropdown,
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Layout,
|
||||||
|
List,
|
||||||
|
Menu,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
notification,
|
||||||
|
pageHeader,
|
||||||
|
Pagination,
|
||||||
|
Popconfirm,
|
||||||
|
Popover,
|
||||||
|
Progress,
|
||||||
|
Radio,
|
||||||
|
Result,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Skeleton,
|
||||||
|
Slider,
|
||||||
|
Spin,
|
||||||
|
Statistic,
|
||||||
|
Steps,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Tooltip,
|
||||||
|
Transfer,
|
||||||
|
Upload,
|
||||||
|
Switch
|
||||||
|
} from 'ant-design-vue'
|
||||||
|
|
||||||
|
Vue.use(ConfigProvider)
|
||||||
|
Vue.use(Layout)
|
||||||
|
Vue.use(Input)
|
||||||
|
Vue.use(InputNumber)
|
||||||
|
Vue.use(Button)
|
||||||
|
Vue.use(Radio)
|
||||||
|
Vue.use(Checkbox)
|
||||||
|
Vue.use(Select)
|
||||||
|
Vue.use(Collapse)
|
||||||
|
Vue.use(Card)
|
||||||
|
Vue.use(Form)
|
||||||
|
Vue.use(Row)
|
||||||
|
Vue.use(Col)
|
||||||
|
Vue.use(Modal)
|
||||||
|
Vue.use(Table)
|
||||||
|
Vue.use(Tabs)
|
||||||
|
Vue.use(Icon)
|
||||||
|
Vue.use(Badge)
|
||||||
|
Vue.use(Popover)
|
||||||
|
Vue.use(Dropdown)
|
||||||
|
Vue.use(List)
|
||||||
|
Vue.use(Avatar)
|
||||||
|
Vue.use(Breadcrumb)
|
||||||
|
Vue.use(Steps)
|
||||||
|
Vue.use(Spin)
|
||||||
|
Vue.use(Menu)
|
||||||
|
Vue.use(Drawer)
|
||||||
|
Vue.use(Tooltip)
|
||||||
|
Vue.use(Alert)
|
||||||
|
Vue.use(Divider)
|
||||||
|
Vue.use(DatePicker)
|
||||||
|
Vue.use(Upload)
|
||||||
|
Vue.use(Progress)
|
||||||
|
Vue.use(Skeleton)
|
||||||
|
Vue.use(Popconfirm)
|
||||||
|
Vue.use(notification)
|
||||||
|
Vue.use(Empty)
|
||||||
|
Vue.use(Statistic)
|
||||||
|
Vue.use(Pagination)
|
||||||
|
Vue.use(Slider)
|
||||||
|
Vue.use(Transfer)
|
||||||
|
Vue.use(Comment)
|
||||||
|
Vue.use(Descriptions)
|
||||||
|
Vue.use(Result)
|
||||||
|
Vue.use(pageHeader)
|
||||||
|
Vue.use(Switch)
|
||||||
|
|
||||||
|
Vue.prototype.$confirm = Modal.confirm
|
||||||
|
Vue.prototype.$message = message
|
||||||
|
Vue.prototype.$notification = notification
|
||||||
|
Vue.prototype.$info = Modal.info
|
||||||
|
Vue.prototype.$success = Modal.success
|
||||||
|
Vue.prototype.$error = Modal.error
|
||||||
|
Vue.prototype.$warning = Modal.warning
|
47
nginx-ui-frontend/src/lib/http/index.js
Normal file
47
nginx-ui-frontend/src/lib/http/index.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import store from '../store'
|
||||||
|
|
||||||
|
/* 创建 axios 实例 */
|
||||||
|
let http = axios.create({
|
||||||
|
baseURL: process.env.VUE_APP_API_ROOT,
|
||||||
|
timeout: 50000,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
transformRequest: [function (data, headers) {
|
||||||
|
if (headers['Content-Type'] === 'multipart/form-data;charset=UTF-8') {
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
return JSON.stringify(data)
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
|
/* http request 拦截器 */
|
||||||
|
http.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* response 拦截器 */
|
||||||
|
http.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
return Promise.resolve(response.data)
|
||||||
|
},
|
||||||
|
async error => {
|
||||||
|
console.log(error)
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
// 无权访问时,直接登出
|
||||||
|
await store.dispatch('logout')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return Promise.reject(error.response.data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default http
|
19
nginx-ui-frontend/src/lib/store/index.js
Normal file
19
nginx-ui-frontend/src/lib/store/index.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import VuexPersistence from 'vuex-persist'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
const debug = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
const vuexLocal = new VuexPersistence({
|
||||||
|
storage: window.localStorage,
|
||||||
|
modules: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
// 将各组件分别模块化存入 Store
|
||||||
|
modules: {},
|
||||||
|
plugins: [vuexLocal.plugin],
|
||||||
|
strict: debug
|
||||||
|
})
|
38
nginx-ui-frontend/src/lib/store/mock.js
Normal file
38
nginx-ui-frontend/src/lib/store/mock.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export const mock = {
|
||||||
|
namespace: true,
|
||||||
|
state: {
|
||||||
|
user: {
|
||||||
|
name: 'mock 用户',
|
||||||
|
school_id: '201904020209',
|
||||||
|
superuser: true,
|
||||||
|
// 0学生 1企业 2教师 3学院
|
||||||
|
power: 2,
|
||||||
|
gender: 1,
|
||||||
|
phone: "10086",
|
||||||
|
email: 'me@jackyu.cn',
|
||||||
|
description: '前端、后端、系统架构',
|
||||||
|
college_id: 1,
|
||||||
|
college_name: "大数据与互联网学院",
|
||||||
|
major: 1,
|
||||||
|
major_name: "物联网工程",
|
||||||
|
position: 'HR'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
update_mock_user(state, payload) {
|
||||||
|
for (const k in payload) {
|
||||||
|
state.user[k] = payload[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async update_mock_user({commit}, data) {
|
||||||
|
commit('update_mock_user', data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
user(state) {
|
||||||
|
return state.user
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
51
nginx-ui-frontend/src/lib/store/user.js
Normal file
51
nginx-ui-frontend/src/lib/store/user.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
export const user = {
|
||||||
|
namespace: true,
|
||||||
|
state: {
|
||||||
|
info: {
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
power: null,
|
||||||
|
college_id: null,
|
||||||
|
college_name: null,
|
||||||
|
major_id: null,
|
||||||
|
major_name: null,
|
||||||
|
position: null
|
||||||
|
},
|
||||||
|
token: null
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
login(state, payload) {
|
||||||
|
state.token = payload.token
|
||||||
|
},
|
||||||
|
logout(state) {
|
||||||
|
sessionStorage.clear()
|
||||||
|
state.info = {}
|
||||||
|
state.token = null
|
||||||
|
},
|
||||||
|
update_user(state, payload) {
|
||||||
|
state.info = payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async login({commit}, data) {
|
||||||
|
commit('login', data)
|
||||||
|
},
|
||||||
|
async logout({commit}) {
|
||||||
|
commit('logout')
|
||||||
|
},
|
||||||
|
async update_user({commit}, data) {
|
||||||
|
commit('update_user', data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
info(state) {
|
||||||
|
return state.info
|
||||||
|
},
|
||||||
|
token(state) {
|
||||||
|
return state.token
|
||||||
|
},
|
||||||
|
isLogin(state) {
|
||||||
|
return !!state.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
nginx-ui-frontend/src/lib/utils/index.js
Normal file
50
nginx-ui-frontend/src/lib/utils/index.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import scrollPosition from './scroll-position'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
install(Vue, options) {
|
||||||
|
Vue.prototype.extend = (target, source) => {
|
||||||
|
for (let obj in source) {
|
||||||
|
target[obj] = source[obj]
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.getClientWidth = () => {
|
||||||
|
return document.body.clientWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.collapse = () => {
|
||||||
|
return !(Vue.prototype.getClientWidth() > 768 || Vue.prototype.getClientWidth() < 512)
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.bytesToSize = (bytes) => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.transformUserType = (power) => {
|
||||||
|
const type = ['学生', '企业', '教师', '学院']
|
||||||
|
return type[power]
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.transformGrade = {
|
||||||
|
7: 'A+',
|
||||||
|
6: 'A',
|
||||||
|
5: 'B+',
|
||||||
|
4: 'B',
|
||||||
|
3: 'C+',
|
||||||
|
2: 'C',
|
||||||
|
1: 'D',
|
||||||
|
0: 'F'
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.scrollPosition = scrollPosition
|
||||||
|
}
|
||||||
|
}
|
27
nginx-ui-frontend/src/lib/utils/scroll-position.js
Normal file
27
nginx-ui-frontend/src/lib/utils/scroll-position.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
let cache = {}
|
||||||
|
|
||||||
|
const scrollPosition = {
|
||||||
|
// 保存滚动条位置
|
||||||
|
save(path) {
|
||||||
|
cache[path] = document.documentElement.scrollTop || document.body.scrollTop
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置滚动条位置
|
||||||
|
get() {
|
||||||
|
const path = this.$route.path
|
||||||
|
Vue.prototype.$nextTick(() => {
|
||||||
|
document.documentElement.scrollTop = document.body.scrollTop = cache[path] || 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置滚动条到顶部
|
||||||
|
goTop() {
|
||||||
|
Vue.prototype.$nextTick(() => {
|
||||||
|
document.documentElement.scrollTop = document.body.scrollTop = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default scrollPosition
|
41
nginx-ui-frontend/src/main.js
Normal file
41
nginx-ui-frontend/src/main.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import store from './lib/store'
|
||||||
|
import '@/lazy'
|
||||||
|
import '@/assets/css/dark.less'
|
||||||
|
import '@/assets/css/style.less'
|
||||||
|
import {router, routes} from './router'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
import utils from '@/lib/utils'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
Vue.use(utils)
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
Vue.prototype.$routeConfig = routes
|
||||||
|
Vue.prototype.$api = api
|
||||||
|
|
||||||
|
NProgress.configure({
|
||||||
|
easing: 'ease',
|
||||||
|
speed: 500,
|
||||||
|
showSpinner: false,
|
||||||
|
trickleSpeed: 200,
|
||||||
|
minimum: 0.3
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
NProgress.start()
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
store,
|
||||||
|
router,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
114
nginx-ui-frontend/src/router/index.js
Normal file
114
nginx-ui-frontend/src/router/index.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: '首页',
|
||||||
|
component: () => import('@/layouts/BaseLayout'),
|
||||||
|
redirect: '/domain',
|
||||||
|
children: [
|
||||||
|
/*{
|
||||||
|
path: 'dashboard',
|
||||||
|
//component: () => import('@/views/dashboard/DashBoard'),
|
||||||
|
name: '仪表盘',
|
||||||
|
meta: {
|
||||||
|
//hiddenHeaderContent: true,
|
||||||
|
icon: 'home'
|
||||||
|
}
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
path: 'domain',
|
||||||
|
name: '网站管理',
|
||||||
|
component: () => import('@/layouts/BaseRouterView'),
|
||||||
|
meta: {
|
||||||
|
icon: 'cloud'
|
||||||
|
},
|
||||||
|
redirect: '/domain/list',
|
||||||
|
children: [{
|
||||||
|
path: 'list',
|
||||||
|
name: '网站列表',
|
||||||
|
component: () => import('@/views/Domain.vue'),
|
||||||
|
}, {
|
||||||
|
path: 'add',
|
||||||
|
name: '添加站点',
|
||||||
|
component: () => import('@/views/DomainEdit.vue'),
|
||||||
|
}, {
|
||||||
|
path: ':name',
|
||||||
|
name: '编辑站点',
|
||||||
|
component: () => import('@/views/DomainEdit.vue'),
|
||||||
|
meta: {
|
||||||
|
hiddenInSidebar: true
|
||||||
|
}
|
||||||
|
}, ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'config',
|
||||||
|
name: '配置管理',
|
||||||
|
component: () => import('@/views/Config.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'file'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'config/:name',
|
||||||
|
name: '配置编辑',
|
||||||
|
component: () => import('@/views/ConfigEdit.vue'),
|
||||||
|
meta: {
|
||||||
|
hiddenInSidebar: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
name: '关于',
|
||||||
|
component: () => import('@/views/About.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'info-circle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
name: '404 Not Found',
|
||||||
|
component: () => import('@/views/Error'),
|
||||||
|
meta: {noAuth: true, status_code: 404, error: 'Not Found'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
name: '未找到页面',
|
||||||
|
redirect: '/404',
|
||||||
|
meta: {noAuth: true}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
document.title = 'Nginx UI | ' + to.name
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
axios.get('/version.json?' + Date.now()).then(r => {
|
||||||
|
if (!(process.env.VUE_APP_VERSION === r.data.version
|
||||||
|
&& Number(process.env.VUE_APP_BUILD_ID) === r.data.build_id)) {
|
||||||
|
Vue.prototype.$info({
|
||||||
|
title: '系统信息',
|
||||||
|
content: '检测到版本更新,将会自动刷新本页',
|
||||||
|
onOk() {
|
||||||
|
location.reload()
|
||||||
|
},
|
||||||
|
okText: '好的'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export {router}
|
48
nginx-ui-frontend/src/views/About.vue
Normal file
48
nginx-ui-frontend/src/views/About.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<a-card>
|
||||||
|
<h2>Nginx UI</h2>
|
||||||
|
<p>Yet another WebUI for Nginx</p>
|
||||||
|
<p>Version: {{ version }}-{{ build_id }}</p>
|
||||||
|
<h3>项目组</h3>
|
||||||
|
<p>前端:0xJacky</p>
|
||||||
|
<p>后端:0xJacky</p>
|
||||||
|
<h3>技术栈</h3>
|
||||||
|
<p>Go</p>
|
||||||
|
<p>Gin</p>
|
||||||
|
<p>Vue</p>
|
||||||
|
|
||||||
|
<p>Copyright © 2020 - {{ this_year }} 0xJacky </p>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'About',
|
||||||
|
data() {
|
||||||
|
const date = new Date()
|
||||||
|
return {
|
||||||
|
this_year: date.getFullYear(),
|
||||||
|
version: process.env.VUE_APP_VERSION,
|
||||||
|
build_id: process.env.VUE_APP_BUILD_ID ? process.env.VUE_APP_BUILD_ID : 'dev',
|
||||||
|
api_root: process.env.VUE_APP_API_ROOT
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async changeUserPower(power) {
|
||||||
|
await this.$store.dispatch('update_mock_user', {power: power})
|
||||||
|
await this.$api.user.info()
|
||||||
|
await this.$message.success("修改成功")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.egg {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
margin: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
53
nginx-ui-frontend/src/views/Config.vue
Normal file
53
nginx-ui-frontend/src/views/Config.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<a-card title="配置文件">
|
||||||
|
<std-table
|
||||||
|
:api="api"
|
||||||
|
:columns="columns"
|
||||||
|
:deletable="false"
|
||||||
|
data_key="configs"
|
||||||
|
@clickEdit="item => {
|
||||||
|
$router.push({
|
||||||
|
path: '/config/' + item.name
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdTable from "@/components/StdDataDisplay/StdTable"
|
||||||
|
|
||||||
|
const columns = [{
|
||||||
|
title: "名称",
|
||||||
|
dataIndex: "name",
|
||||||
|
scopedSlots: {customRender: "名称"},
|
||||||
|
sorter: true,
|
||||||
|
pithy: true
|
||||||
|
}, {
|
||||||
|
title: "修改时间",
|
||||||
|
dataIndex: "modify",
|
||||||
|
datetime: true,
|
||||||
|
scopedSlots: {customRender: "modify"},
|
||||||
|
sorter: true,
|
||||||
|
pithy: true
|
||||||
|
}, {
|
||||||
|
title: "操作",
|
||||||
|
dataIndex: "action",
|
||||||
|
scopedSlots: {customRender: "action"}
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Config",
|
||||||
|
components: {StdTable},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
api: this.$api.config,
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
68
nginx-ui-frontend/src/views/ConfigEdit.vue
Normal file
68
nginx-ui-frontend/src/views/ConfigEdit.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<a-card title="配置文件实时编辑">
|
||||||
|
<a-textarea v-model="configText" :rows="36"/>
|
||||||
|
<footer-tool-bar>
|
||||||
|
<a-button type="primary" @click="save">保存</a-button>
|
||||||
|
</footer-tool-bar>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FooterToolBar from "@/components/FooterToolbar/FooterToolBar"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DomainEdit",
|
||||||
|
components: {FooterToolBar},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: this.$route.params.name,
|
||||||
|
configText: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.config = {}
|
||||||
|
this.configText = ""
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
handler() {
|
||||||
|
this.unparse()
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.name) {
|
||||||
|
this.$api.config.get(this.name).then(r => {
|
||||||
|
this.configText = r.config
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("服务器错误")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.configText = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
save() {
|
||||||
|
this.$api.config.save(this.name ? this.name : this.config.name, {content: this.configText}).then(r => {
|
||||||
|
this.configText = r.config
|
||||||
|
this.$message.success("保存成功")
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("保存错误")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.ant-card {
|
||||||
|
margin: 10px;
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
88
nginx-ui-frontend/src/views/Domain.vue
Normal file
88
nginx-ui-frontend/src/views/Domain.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<a-card title="网站管理">
|
||||||
|
<std-table
|
||||||
|
:api="api"
|
||||||
|
:columns="columns"
|
||||||
|
data_key="configs"
|
||||||
|
ref="table"
|
||||||
|
@clickEdit="r => this.$router.push({
|
||||||
|
path: '/domain/' + r.name
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
<template #action="{record}">
|
||||||
|
<a v-if="record.enabled" @click="disable(record.name)">禁用</a>
|
||||||
|
<a v-else @click="enable(record.name)">启用</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
</template>
|
||||||
|
</std-table>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdTable from "@/components/StdDataDisplay/StdTable"
|
||||||
|
|
||||||
|
const columns = [{
|
||||||
|
title: "名称",
|
||||||
|
dataIndex: "name",
|
||||||
|
scopedSlots: {customRender: "名称"},
|
||||||
|
sorter: true,
|
||||||
|
pithy: true
|
||||||
|
}, {
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "enabled",
|
||||||
|
badge: true,
|
||||||
|
scopedSlots: {customRender: "enabled"},
|
||||||
|
mask: {
|
||||||
|
true: "启用",
|
||||||
|
false: "未启用"
|
||||||
|
},
|
||||||
|
sorter: true,
|
||||||
|
pithy: true
|
||||||
|
}, {
|
||||||
|
title: "修改时间",
|
||||||
|
dataIndex: "modify",
|
||||||
|
datetime: true,
|
||||||
|
scopedSlots: {customRender: "modify"},
|
||||||
|
sorter: true,
|
||||||
|
pithy: true
|
||||||
|
}, {
|
||||||
|
title: "操作",
|
||||||
|
dataIndex: "action",
|
||||||
|
scopedSlots: {customRender: "action"}
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Domain",
|
||||||
|
components: {StdTable},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
api: this.$api.domain,
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
enable(name) {
|
||||||
|
this.$api.domain.enable(name).then(() => {
|
||||||
|
this.$message.success("启用成功")
|
||||||
|
this.$refs.table.get_list()
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("启用失败")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disable(name) {
|
||||||
|
this.$api.domain.disable(name).then(() => {
|
||||||
|
this.$message.success("禁用成功")
|
||||||
|
this.$refs.table.get_list()
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("禁用失败")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
237
nginx-ui-frontend/src/views/DomainEdit.vue
Normal file
237
nginx-ui-frontend/src/views/DomainEdit.vue
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
<template>
|
||||||
|
<a-row>
|
||||||
|
<a-col :md="12" :sm="24">
|
||||||
|
<a-card :title="name ? '编辑站点:' + name : '添加站点'">
|
||||||
|
<std-data-entry :data-list="columns" v-model="config" @change_support_ssl="change_support_ssl"/>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="12" :sm="24">
|
||||||
|
<a-card title="配置文件实时编辑">
|
||||||
|
<a-textarea v-model="configText" :rows="36"/>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<footer-tool-bar>
|
||||||
|
<a-button type="primary" @click="save">保存</a-button>
|
||||||
|
</footer-tool-bar>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StdDataEntry from "@/components/StdDataEntry/StdDataEntry"
|
||||||
|
import FooterToolBar from "@/components/FooterToolbar/FooterToolBar"
|
||||||
|
|
||||||
|
const columns = [{
|
||||||
|
title: "配置文件名称",
|
||||||
|
dataIndex: "name",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "网站域名 (server_name)",
|
||||||
|
dataIndex: "server_name",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "http 监听端口",
|
||||||
|
dataIndex: "http_listen_port",
|
||||||
|
edit: {
|
||||||
|
type: "number",
|
||||||
|
min: 80
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "支持 SSL",
|
||||||
|
dataIndex: "support_ssl",
|
||||||
|
edit: {
|
||||||
|
type: "switch",
|
||||||
|
event: "change_support_ssl"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "https 监听端口",
|
||||||
|
dataIndex: "https_listen_port",
|
||||||
|
edit: {
|
||||||
|
type: "number",
|
||||||
|
min: 443
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "SSL 证书路径 (ssl_certificate)",
|
||||||
|
dataIndex: "ssl_certificate",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "SSL 证书私钥路径 (ssl_certificate_key)",
|
||||||
|
dataIndex: "ssl_certificate_key",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "网站根目录 (root)",
|
||||||
|
dataIndex: "root",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: "网站首页 (index)",
|
||||||
|
dataIndex: "index",
|
||||||
|
edit: {
|
||||||
|
type: "input"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DomainEdit",
|
||||||
|
components: {FooterToolBar, StdDataEntry},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: this.$route.params.name,
|
||||||
|
columns,
|
||||||
|
config: {
|
||||||
|
http_listen_port: 80,
|
||||||
|
https_listen_port: 443,
|
||||||
|
server_name: "",
|
||||||
|
index: "",
|
||||||
|
root: "",
|
||||||
|
ssl_certificate: "",
|
||||||
|
ssl_certificate_key: "",
|
||||||
|
support_ssl: false
|
||||||
|
},
|
||||||
|
configText: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.config = {}
|
||||||
|
this.configText = ""
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
handler() {
|
||||||
|
this.unparse()
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.name) {
|
||||||
|
this.$api.domain.get(this.name).then(r => {
|
||||||
|
this.configText = r.config
|
||||||
|
this.parse(r.config)
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("服务器错误")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.config = {
|
||||||
|
http_listen_port: 80,
|
||||||
|
https_listen_port: 443,
|
||||||
|
server_name: "",
|
||||||
|
index: "",
|
||||||
|
root: "",
|
||||||
|
ssl_certificate: "",
|
||||||
|
ssl_certificate_key: "",
|
||||||
|
support_ssl: false
|
||||||
|
}
|
||||||
|
this.get_template()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
parse(r) {
|
||||||
|
const text = r.config
|
||||||
|
const reg = {
|
||||||
|
http_listen_port: /listen[\s](.*);/i,
|
||||||
|
https_listen_port: /listen[\s](.*) ssl/i,
|
||||||
|
server_name: /server_name[\s](.*);/i,
|
||||||
|
index: /index[\s](.*);/i,
|
||||||
|
root: /root[\s](.*);/i,
|
||||||
|
ssl_certificate: /ssl_certificate[\s](.*);/i,
|
||||||
|
ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
|
||||||
|
}
|
||||||
|
this.config['name'] = r.name
|
||||||
|
for (let r in reg) {
|
||||||
|
const match = text.match(reg[r])
|
||||||
|
// console.log(r, match)
|
||||||
|
if (match !== null) {
|
||||||
|
if (match[1] !== undefined) {
|
||||||
|
this.config[r] = match[1]
|
||||||
|
} else {
|
||||||
|
this.config[r] = match[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.config.https_listen_port) {
|
||||||
|
this.config.support_ssl = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unparse() {
|
||||||
|
let text = this.configText
|
||||||
|
// http_listen_port: /listen (.*);/i,
|
||||||
|
// https_listen_port: /listen (.*) ssl/i,
|
||||||
|
const reg = {
|
||||||
|
server_name: /server_name[\s](.*);/ig,
|
||||||
|
index: /index[\s](.*);/i,
|
||||||
|
root: /root[\s](.*);/i,
|
||||||
|
ssl_certificate: /ssl_certificate[\s](.*);/i,
|
||||||
|
ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
|
||||||
|
}
|
||||||
|
text = text.replace(/listen[\s](.*);/i, "listen\t"
|
||||||
|
+ this.config['http_listen_port'] + ';')
|
||||||
|
text = text.replace(/listen[\s](.*) ssl/i, "listen\t"
|
||||||
|
+ this.config['https_listen_port'] + ' ssl')
|
||||||
|
|
||||||
|
text = text.replace(/listen(.*):(.*);/i, "listen\t[::]:"
|
||||||
|
+ this.config['http_listen_port'] + ';')
|
||||||
|
text = text.replace(/listen(.*):(.*) ssl/i, "listen\t[::]:"
|
||||||
|
+ this.config['https_listen_port'] + ' ssl')
|
||||||
|
|
||||||
|
for (let k in reg) {
|
||||||
|
text = text.replace(new RegExp(reg[k]), k + "\t" +
|
||||||
|
(this.config[k] !== undefined ? this.config[k] : " ") + ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configText = text
|
||||||
|
},
|
||||||
|
async get_template() {
|
||||||
|
if (this.config.support_ssl) {
|
||||||
|
await this.$api.domain.get_template('https-conf').then(r => {
|
||||||
|
this.configText = r.template
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.$api.domain.get_template('http-conf').then(r => {
|
||||||
|
this.configText = r.template
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await this.unparse()
|
||||||
|
},
|
||||||
|
change_support_ssl() {
|
||||||
|
const that = this
|
||||||
|
this.$confirm({
|
||||||
|
title: '您已修改 SSL 支持状态,是否需要更换配置文件模板?',
|
||||||
|
content: '更换配置文件模板将会丢失自定义配置',
|
||||||
|
onOk() {
|
||||||
|
that.get_template()
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.$api.domain.save(this.name ? this.name : this.config.name, {content: this.configText}).then(r => {
|
||||||
|
this.parse(r)
|
||||||
|
this.$message.success("保存成功")
|
||||||
|
}).catch(r => {
|
||||||
|
console.log(r)
|
||||||
|
this.$message.error("保存错误")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.ant-card {
|
||||||
|
margin: 10px;
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
62
nginx-ui-frontend/src/views/Error.vue
Normal file
62
nginx-ui-frontend/src/views/Error.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<h1 class="title">{{ $route.meta.status_code ? $route.meta.status_code : 404 }}</h1>
|
||||||
|
<p>{{ $route.meta.error ? $route.meta.error : '找不到文件' }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Error'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
body, div, h1, html {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
color: #444;
|
||||||
|
position: relative;
|
||||||
|
font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, CustomFont, "Microsoft YaHei UI", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
|
||||||
|
background: #fcfcfc;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 8em;
|
||||||
|
font-weight: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #4181b9;
|
||||||
|
text-decoration: none;
|
||||||
|
-webkit-transition: all .3s ease;
|
||||||
|
-moz-transition: all .3s ease;
|
||||||
|
-ms-transition: all .3s ease;
|
||||||
|
-o-transition: all .3s ease;
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:active, &:hover {
|
||||||
|
color: #5bb0ed
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 400;
|
||||||
|
width: 100%;
|
||||||
|
height: 30%;
|
||||||
|
line-height: 1;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
</style>
|
18
nginx-ui-frontend/src/views/Home.vue
Normal file
18
nginx-ui-frontend/src/views/Home.vue
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<img alt="Vue logo" src="../assets/img/logo.png">
|
||||||
|
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// @ is an alias to /src
|
||||||
|
import HelloWorld from '@/components/HelloWorld.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
components: {
|
||||||
|
HelloWorld
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
4
nginx-ui-frontend/version.json
Normal file
4
nginx-ui-frontend/version.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"build_id": 1
|
||||||
|
}
|
53
nginx-ui-frontend/vue.config.js
Normal file
53
nginx-ui-frontend/vue.config.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
pages: {
|
||||||
|
index: {
|
||||||
|
// pages 的入口
|
||||||
|
entry: 'src/main.js',
|
||||||
|
// 模板来源
|
||||||
|
template: 'public/index.html',
|
||||||
|
// 在 dist/index.html 的输出
|
||||||
|
filename: 'index.html',
|
||||||
|
// 当使用 title 选项时,
|
||||||
|
// template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
title: 'Nginx UI',
|
||||||
|
// 在这个页面中包含的块,默认情况下会包含
|
||||||
|
// 提取出来的通用 chunk 和 vendor chunk。
|
||||||
|
chunks: ['chunk-vendors', 'chunk-common', 'index']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
proxy: 'http://localhost:9000'
|
||||||
|
},
|
||||||
|
|
||||||
|
productionSourceMap: false,
|
||||||
|
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
css: {},
|
||||||
|
postcss: {},
|
||||||
|
less: {
|
||||||
|
javascriptEnabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extract: false
|
||||||
|
},
|
||||||
|
|
||||||
|
configureWebpack: config => {
|
||||||
|
config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
config.performance = {
|
||||||
|
hints: 'warning',
|
||||||
|
// 入口起点的最大体积
|
||||||
|
maxEntrypointSize: 50000000,
|
||||||
|
// 生成文件的最大体积
|
||||||
|
maxAssetSize: 30000000,
|
||||||
|
// 只给出 js 文件的性能提示
|
||||||
|
assetFilter: function (assetFilename) {
|
||||||
|
return assetFilename.endsWith('.js')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8880
nginx-ui-frontend/yarn.lock
Normal file
8880
nginx-ui-frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,19 +1,25 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/model"
|
"github.com/0xJacky/Nginx-UI/tool"
|
||||||
"github.com/0xJacky/Nginx-UI/tool"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin"
|
"io/ioutil"
|
||||||
"io/ioutil"
|
"log"
|
||||||
"log"
|
"net/http"
|
||||||
"net/http"
|
"os"
|
||||||
"os"
|
"path/filepath"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetConfigs(c *gin.Context) {
|
func GetConfigs(c *gin.Context) {
|
||||||
configFiles, err := ioutil.ReadDir(tool.GetNginxConfPath("/"))
|
orderBy := c.Query("order_by")
|
||||||
|
sort := c.DefaultQuery("sort", "desc")
|
||||||
|
|
||||||
|
mySort := map[string]string{
|
||||||
|
"name": "string",
|
||||||
|
"modify": "time",
|
||||||
|
}
|
||||||
|
|
||||||
|
configFiles, err := ioutil.ReadDir(tool.GetNginxConfPath("/"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorHandler(c, err)
|
ErrorHandler(c, err)
|
||||||
|
@ -34,6 +40,8 @@ func GetConfigs(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"configs": configs,
|
"configs": configs,
|
||||||
})
|
})
|
||||||
|
@ -114,21 +122,15 @@ func EditConfig(c *gin.Context) {
|
||||||
path := filepath.Join(tool.GetNginxConfPath("/"), name)
|
path := filepath.Join(tool.GetNginxConfPath("/"), name)
|
||||||
content := request.Content
|
content := request.Content
|
||||||
|
|
||||||
s, err := strconv.Unquote(`"` + content + `"`)
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
origContent, err := ioutil.ReadFile(path)
|
origContent, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorHandler(c, err)
|
ErrorHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s != "" && s != string(origContent) {
|
if content != "" && content != string(origContent) {
|
||||||
model.CreateBackup(path)
|
// model.CreateBackup(path)
|
||||||
err := ioutil.WriteFile(path, []byte(s), 0644)
|
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorHandler(c, err)
|
ErrorHandler(c, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/model"
|
"github.com/0xJacky/Nginx-UI/tool"
|
||||||
"github.com/0xJacky/Nginx-UI/tool"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin"
|
"io/ioutil"
|
||||||
"io/ioutil"
|
"net/http"
|
||||||
"log"
|
"os"
|
||||||
"net/http"
|
"path/filepath"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDomains(c *gin.Context) {
|
func GetDomains(c *gin.Context) {
|
||||||
|
orderBy := c.Query("order_by")
|
||||||
|
sort := c.DefaultQuery("sort", "desc")
|
||||||
|
|
||||||
|
mySort := map[string]string{
|
||||||
|
"enabled": "bool",
|
||||||
|
"name": "string",
|
||||||
|
"modify": "time",
|
||||||
|
}
|
||||||
|
|
||||||
configFiles, err := ioutil.ReadDir(tool.GetNginxConfPath("sites-available"))
|
configFiles, err := ioutil.ReadDir(tool.GetNginxConfPath("sites-available"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,6 +52,8 @@ func GetDomains(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"configs": configs,
|
"configs": configs,
|
||||||
})
|
})
|
||||||
|
@ -67,6 +75,7 @@ func GetDomain(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
"message": err.Error(),
|
"message": err.Error(),
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ErrorHandler(c, err)
|
ErrorHandler(c, err)
|
||||||
return
|
return
|
||||||
|
@ -74,81 +83,8 @@ func GetDomain(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"config": string(content),
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddDomain(c *gin.Context) {
|
|
||||||
request := make(gin.H)
|
|
||||||
err := c.BindJSON(&request)
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := request["name"].(string)
|
|
||||||
path := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
|
||||||
log.Println(path)
|
|
||||||
if _, err = os.Stat(path); err == nil {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "site exist",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SupportSSL := request["support_ssl"]
|
|
||||||
|
|
||||||
baseKeys := []string{"http_listen_port",
|
|
||||||
"https_listen_port",
|
|
||||||
"server_name",
|
|
||||||
"ssl_certificate", "ssl_certificate_key",
|
|
||||||
"index", "root", "extra",
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp, err := ioutil.ReadFile("template/http-conf")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if SupportSSL == true {
|
|
||||||
tmp, err = ioutil.ReadFile("template/https-conf")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
template := string(tmp)
|
|
||||||
|
|
||||||
content := template
|
|
||||||
|
|
||||||
for i := range baseKeys {
|
|
||||||
val, ok := request[baseKeys[i]]
|
|
||||||
replace := ""
|
|
||||||
if ok {
|
|
||||||
replace = val.(string)
|
|
||||||
}
|
|
||||||
content = strings.Replace(content, "{{ "+baseKeys[i]+" }}",
|
|
||||||
replace, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(name, content)
|
|
||||||
|
|
||||||
err = ioutil.WriteFile(path, []byte(content), 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"name": name,
|
"name": name,
|
||||||
"content": content,
|
"config": string(content),
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -161,21 +97,16 @@ func EditDomain(c *gin.Context) {
|
||||||
err = c.BindJSON(&request)
|
err = c.BindJSON(&request)
|
||||||
path := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
path := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
||||||
|
|
||||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
if _, err = os.Stat(path); err == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
origContent, err = ioutil.ReadFile(path)
|
||||||
"message": "site not found",
|
if err != nil {
|
||||||
})
|
ErrorHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
origContent, err = ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
ErrorHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if request["content"] != "" && request["content"] != string(origContent) {
|
if request["content"] != "" && request["content"] != string(origContent) {
|
||||||
model.CreateBackup(path)
|
// model.CreateBackup(path)
|
||||||
err := ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
|
err := ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorHandler(c, err)
|
ErrorHandler(c, err)
|
||||||
|
@ -242,8 +173,6 @@ func DisableDomain(c *gin.Context) {
|
||||||
func DeleteDomain(c *gin.Context) {
|
func DeleteDomain(c *gin.Context) {
|
||||||
var err error
|
var err error
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
request := make(gin.H)
|
|
||||||
err = c.BindJSON(request)
|
|
||||||
availablePath := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
availablePath := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
||||||
enabledPath := filepath.Join(tool.GetNginxConfPath("sites-enabled"), name)
|
enabledPath := filepath.Join(tool.GetNginxConfPath("sites-enabled"), name)
|
||||||
|
|
||||||
|
|
31
server/api/template.go
Normal file
31
server/api/template.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTemplate(c *gin.Context) {
|
||||||
|
name := c.Param("name")
|
||||||
|
path := filepath.Join("template", name)
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ErrorHandler(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "ok",
|
||||||
|
"template": string(content),
|
||||||
|
})
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ func InitRouter() *gin.Engine {
|
||||||
{
|
{
|
||||||
endpoint.GET("domains", api.GetDomains)
|
endpoint.GET("domains", api.GetDomains)
|
||||||
endpoint.GET("domain/:name", api.GetDomain)
|
endpoint.GET("domain/:name", api.GetDomain)
|
||||||
endpoint.POST("domain", api.AddDomain)
|
|
||||||
endpoint.POST("domain/:name", api.EditDomain)
|
endpoint.POST("domain/:name", api.EditDomain)
|
||||||
endpoint.POST("domain/:name/enable", api.EnableDomain)
|
endpoint.POST("domain/:name/enable", api.EnableDomain)
|
||||||
endpoint.POST("domain/:name/disable", api.DisableDomain)
|
endpoint.POST("domain/:name/disable", api.DisableDomain)
|
||||||
|
@ -35,6 +34,8 @@ func InitRouter() *gin.Engine {
|
||||||
|
|
||||||
endpoint.GET("backups", api.GetFileBackupList)
|
endpoint.GET("backups", api.GetFileBackupList)
|
||||||
endpoint.GET("backup/:id", api.GetFileBackup)
|
endpoint.GET("backup/:id", api.GetFileBackup)
|
||||||
|
|
||||||
|
endpoint.GET("template/:name", api.GetTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
|
@ -4,10 +4,7 @@ server {
|
||||||
|
|
||||||
server_name {{ server_name }};
|
server_name {{ server_name }};
|
||||||
|
|
||||||
{{ index }}
|
root ;
|
||||||
|
|
||||||
{{ root }}
|
index ;
|
||||||
|
|
||||||
# extra
|
|
||||||
{{ extra }}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,10 @@ server {
|
||||||
|
|
||||||
server_name {{ server_name }};
|
server_name {{ server_name }};
|
||||||
|
|
||||||
{{ index }}
|
ssl_certificate {{ ssl_certificate }};
|
||||||
|
ssl_certificate_key {{ ssl_certificate_key }};
|
||||||
|
|
||||||
{{ root }}
|
root ;
|
||||||
|
|
||||||
# extra
|
index ;
|
||||||
{{ extra }}
|
|
||||||
}
|
}
|
||||||
|
|
62
server/tool/configList.go
Normal file
62
server/tool/configList.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MapsSort struct {
|
||||||
|
Key string
|
||||||
|
Type string
|
||||||
|
Order string
|
||||||
|
MapList []gin.H
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MapsSort) Len() int {
|
||||||
|
return len(m.MapList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MapsSort) Less(i, j int) bool {
|
||||||
|
flag := false
|
||||||
|
|
||||||
|
if m.Type == "int" {
|
||||||
|
flag = m.MapList[i][m.Key].(int) > m.MapList[j][m.Key].(int)
|
||||||
|
} else if m.Type == "bool" {
|
||||||
|
flag = boolToInt(m.MapList[i][m.Key].(bool)) > boolToInt(m.MapList[j][m.Key].(bool))
|
||||||
|
} else if m.Type == "bool" {
|
||||||
|
flag = m.MapList[i][m.Key].(string) > m.MapList[j][m.Key].(string)
|
||||||
|
} else if m.Type == "time" {
|
||||||
|
flag = m.MapList[i][m.Key].(time.Time).After(m.MapList[j][m.Key].(time.Time))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Order == "asc" {
|
||||||
|
flag = !flag
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MapsSort) Swap(i, j int) {
|
||||||
|
m.MapList[i], m.MapList[j] = m.MapList[j], m.MapList[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sort(key string, order string, Type string, maps []gin.H) []gin.H {
|
||||||
|
mapsSort := MapsSort{
|
||||||
|
Key: key,
|
||||||
|
MapList: maps,
|
||||||
|
Type: Type,
|
||||||
|
Order: order,
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(mapsSort)
|
||||||
|
|
||||||
|
return mapsSort.MapList
|
||||||
|
}
|
|
@ -1,24 +1,20 @@
|
||||||
package tool
|
package tool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"log"
|
||||||
"log"
|
"os/exec"
|
||||||
"os/exec"
|
"path/filepath"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ReloadNginx() {
|
func ReloadNginx() {
|
||||||
cmd := exec.Command("systemctl", "reload nginx")
|
out, err := exec.Command("nginx", "-s", "reload").CombinedOutput()
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(out.String())
|
log.Println(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetNginxConfPath(dir string) string {
|
func GetNginxConfPath(dir string) string {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue