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
|
||||
database.db
|
||||
|
||||
|
||||
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,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/tool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
|
@ -9,10 +8,17 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetConfigs(c *gin.Context) {
|
||||
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 {
|
||||
|
@ -34,6 +40,8 @@ func GetConfigs(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"configs": configs,
|
||||
})
|
||||
|
@ -114,21 +122,15 @@ func EditConfig(c *gin.Context) {
|
|||
path := filepath.Join(tool.GetNginxConfPath("/"), name)
|
||||
content := request.Content
|
||||
|
||||
s, err := strconv.Unquote(`"` + content + `"`)
|
||||
if err != nil {
|
||||
ErrorHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
origContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
ErrorHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if s != "" && s != string(origContent) {
|
||||
model.CreateBackup(path)
|
||||
err := ioutil.WriteFile(path, []byte(s), 0644)
|
||||
if content != "" && content != string(origContent) {
|
||||
// model.CreateBackup(path)
|
||||
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
ErrorHandler(c, err)
|
||||
return
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/tool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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"))
|
||||
|
||||
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{
|
||||
"configs": configs,
|
||||
})
|
||||
|
@ -67,6 +75,7 @@ func GetDomain(c *gin.Context) {
|
|||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ErrorHandler(c, err)
|
||||
return
|
||||
|
@ -74,81 +83,8 @@ func GetDomain(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"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,
|
||||
"content": content,
|
||||
"config": string(content),
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -161,21 +97,16 @@ func EditDomain(c *gin.Context) {
|
|||
err = c.BindJSON(&request)
|
||||
path := filepath.Join(tool.GetNginxConfPath("sites-available"), name)
|
||||
|
||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"message": "site not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
origContent, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
ErrorHandler(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if request["content"] != "" && request["content"] != string(origContent) {
|
||||
model.CreateBackup(path)
|
||||
// model.CreateBackup(path)
|
||||
err := ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
|
||||
if err != nil {
|
||||
ErrorHandler(c, err)
|
||||
|
@ -242,8 +173,6 @@ func DisableDomain(c *gin.Context) {
|
|||
func DeleteDomain(c *gin.Context) {
|
||||
var err error
|
||||
name := c.Param("name")
|
||||
request := make(gin.H)
|
||||
err = c.BindJSON(request)
|
||||
availablePath := filepath.Join(tool.GetNginxConfPath("sites-available"), 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("domain/:name", api.GetDomain)
|
||||
endpoint.POST("domain", api.AddDomain)
|
||||
endpoint.POST("domain/:name", api.EditDomain)
|
||||
endpoint.POST("domain/:name/enable", api.EnableDomain)
|
||||
endpoint.POST("domain/:name/disable", api.DisableDomain)
|
||||
|
@ -35,6 +34,8 @@ func InitRouter() *gin.Engine {
|
|||
|
||||
endpoint.GET("backups", api.GetFileBackupList)
|
||||
endpoint.GET("backup/:id", api.GetFileBackup)
|
||||
|
||||
endpoint.GET("template/:name", api.GetTemplate)
|
||||
}
|
||||
|
||||
return r
|
||||
|
|
|
@ -4,10 +4,7 @@ server {
|
|||
|
||||
server_name {{ server_name }};
|
||||
|
||||
{{ index }}
|
||||
root ;
|
||||
|
||||
{{ root }}
|
||||
|
||||
# extra
|
||||
{{ extra }}
|
||||
index ;
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ server {
|
|||
|
||||
server_name {{ server_name }};
|
||||
|
||||
{{ index }}
|
||||
ssl_certificate {{ ssl_certificate }};
|
||||
ssl_certificate_key {{ ssl_certificate_key }};
|
||||
|
||||
{{ root }}
|
||||
root ;
|
||||
|
||||
# extra
|
||||
{{ extra }}
|
||||
index ;
|
||||
}
|
||||
|
|
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,7 +1,6 @@
|
|||
package tool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -9,16 +8,13 @@ import (
|
|||
)
|
||||
|
||||
func ReloadNginx() {
|
||||
cmd := exec.Command("systemctl", "reload nginx")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
out, err := exec.Command("nginx", "-s", "reload").CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
log.Println(out.String())
|
||||
log.Println(string(out))
|
||||
}
|
||||
|
||||
func GetNginxConfPath(dir string) string {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue