Browse Source

oauth2.0认证

tuonian 1 year ago
parent
commit
42c101ba2b
100 changed files with 7078 additions and 6748 deletions
  1. 14 14
      .eslintrc.cjs
  2. 36 34
      .gitignore
  3. 21 21
      LICENSE
  4. 93 91
      README.md
  5. 3 3
      build-nginx-with-ui.sh
  6. 18 18
      build-server.sh
  7. 3 3
      build.sh
  8. 6 6
      dev.sh
  9. 0 8
      dist/assets/index-96b4a255.js
  10. 0 0
      dist/assets/index-976dda2b.css
  11. 3 3
      dist/config.js
  12. 45 45
      dist/index.html
  13. 22 22
      docker-compose-dev.yaml
  14. 16 16
      docker-compose.yaml
  15. 29 29
      docker/Dockerfile
  16. 30 30
      docker/Dockerfile-with-nginx
  17. 15 15
      docker/entrypoint.sh
  18. 14 14
      index.html
  19. 64 64
      package.json
  20. 3 3
      public/config.js
  21. 19 3
      server/conf/app.conf
  22. 111 40
      server/config/config.go
  23. 104 85
      server/controllers/default.go
  24. 255 250
      server/controllers/nginx.go
  25. 81 0
      server/controllers/oauth2.go
  26. 66 0
      server/controllers/user.go
  27. BIN
      server/data/db/sqlite.db
  28. BIN
      server/data/sessions/4/0/40000f9d991d9b84f2cf3d7778d41f75
  29. BIN
      server/data/sessions/d/c/dcab0e00bab02c9ea3c863a59d2bb126
  30. 8 4
      server/go.mod
  31. 21 0
      server/go.sum
  32. 2 2
      server/main.go
  33. 23 5
      server/middleware/auth.go
  34. 8 5
      server/routers/router.go
  35. 42 12
      server/utils/cert.go
  36. 41 41
      src/App.css
  37. 28 28
      src/App.tsx
  38. 27 27
      src/adapter/index.js
  39. 119 119
      src/api/nginx.ts
  40. 68 68
      src/api/request.ts
  41. 29 29
      src/components/BackButton.tsx
  42. 699 699
      src/config/nginx_form.json
  43. 68 68
      src/config/nginx_template.json
  44. 75 75
      src/index.css
  45. 56 56
      src/main.tsx
  46. 41 41
      src/models/api.ts
  47. 279 279
      src/models/nginx.ts
  48. 52 52
      src/pages/nginx/certs/index.less
  49. 249 249
      src/pages/nginx/certs/index.tsx
  50. 54 54
      src/pages/nginx/components/EditNginxBtn.tsx
  51. 97 97
      src/pages/nginx/components/StopStartButton.tsx
  52. 39 39
      src/pages/nginx/components/auth/config.json
  53. 10 10
      src/pages/nginx/components/auth/index.less
  54. 40 40
      src/pages/nginx/components/auth/index.tsx
  55. 19 19
      src/pages/nginx/components/basic/index.less
  56. 108 108
      src/pages/nginx/components/basic/index.tsx
  57. 52 52
      src/pages/nginx/components/certs/index.tsx
  58. 53 53
      src/pages/nginx/components/cors/config.json
  59. 34 34
      src/pages/nginx/components/cors/index.less
  60. 110 110
      src/pages/nginx/components/cors/index.tsx
  61. 32 32
      src/pages/nginx/components/error/config.json
  62. 2 2
      src/pages/nginx/components/error/index.less
  63. 73 73
      src/pages/nginx/components/error/index.tsx
  64. 64 64
      src/pages/nginx/components/gzip/config.json
  65. 10 10
      src/pages/nginx/components/gzip/index.less
  66. 97 97
      src/pages/nginx/components/gzip/index.tsx
  67. 9 9
      src/pages/nginx/components/index.ts
  68. 28 28
      src/pages/nginx/components/input.ts
  69. 271 271
      src/pages/nginx/components/location/config.json
  70. 41 41
      src/pages/nginx/components/location/index.less
  71. 280 280
      src/pages/nginx/components/location/index.tsx
  72. 107 107
      src/pages/nginx/components/location/utils.ts
  73. 195 195
      src/pages/nginx/components/proxy/config.json
  74. 29 29
      src/pages/nginx/components/proxy/index.less
  75. 87 87
      src/pages/nginx/components/proxy/index.tsx
  76. 70 70
      src/pages/nginx/components/proxy/utils.ts
  77. 31 31
      src/pages/nginx/components/proxypass/index.less
  78. 97 97
      src/pages/nginx/components/proxypass/index.tsx
  79. 70 70
      src/pages/nginx/components/proxypass/stream.tsx
  80. 335 335
      src/pages/nginx/components/site/components/Dragger.tsx
  81. 59 59
      src/pages/nginx/components/site/components/dragger.less
  82. 25 25
      src/pages/nginx/components/site/index.less
  83. 123 123
      src/pages/nginx/components/site/index.tsx
  84. 42 42
      src/pages/nginx/config.tsx
  85. 63 63
      src/pages/nginx/help/args.mdx
  86. 22 22
      src/pages/nginx/help/index.less
  87. 25 25
      src/pages/nginx/help/index.tsx
  88. 71 71
      src/pages/nginx/http/components/HttpConfSync.tsx
  89. 26 26
      src/pages/nginx/http/components/index.less
  90. 126 126
      src/pages/nginx/http/index.tsx
  91. 143 143
      src/pages/nginx/http/utils.ts
  92. 37 37
      src/pages/nginx/index.less
  93. 131 131
      src/pages/nginx/index.tsx
  94. 173 173
      src/pages/nginx/list.tsx
  95. 162 162
      src/pages/nginx/location/index.tsx
  96. 12 12
      src/pages/nginx/location/new.tsx
  97. 75 75
      src/pages/nginx/nginx.json
  98. 70 70
      src/pages/nginx/server/components/SyncButton.tsx
  99. 28 28
      src/pages/nginx/server/components/preview.less
  100. 115 115
      src/pages/nginx/server/components/preview.tsx

+ 14 - 14
.eslintrc.cjs

@@ -1,14 +1,14 @@
-module.exports = {
-  env: { browser: true, es2020: true },
-  extends: [
-    'eslint:recommended',
-    'plugin:@typescript-eslint/recommended',
-    'plugin:react-hooks/recommended',
-  ],
-  parser: '@typescript-eslint/parser',
-  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
-  plugins: ['react-refresh'],
-  rules: {
-    'react-refresh/only-export-components': 'warn',
-  },
-}
+module.exports = {
+  env: { browser: true, es2020: true },
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+  plugins: ['react-refresh'],
+  rules: {
+    'react-refresh/only-export-components': 'warn',
+  },
+}

+ 36 - 34
.gitignore

@@ -1,34 +1,36 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-/local
-
-node_modules
-#dist
-#dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-
-*/.idea
-
-docker/data
-
-server/out
-server/data/files
-
-nginx-ui.tar.gz
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+/local
+
+node_modules
+#dist
+#dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*/.idea
+
+docker/data
+
+server/out
+server/data/files
+
+nginx-ui.tar.gz
+
+server/conf/app.local.conf

+ 21 - 21
LICENSE

@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2023 niantuo
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+MIT License
+
+Copyright (c) 2023 niantuo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 93 - 91
README.md

@@ -1,91 +1,93 @@
-# nginx 可视化界面
-项目的主要功能未nginx的配置管理,通过可视化的界面去配置nginx,所有的配置渲染逻辑都在前端进行,通过后台服务渲染到部署nginx的服务器上;\
-由于nginx的配置实在是太多了,只是可视化了部分常用的功能;\
-可以用于开发环境,需要经常变动一些配置信息的场景.
-
-- 项目的web前端基于react开发,使用vite构建工具;
-- 后端使用golang语言开发(菜鸟学习中)
-
-## 构建
-项目构建基于docker-compose, 分为两种情况
-- 基础镜像为nginx
-  容器自带nginx,启用web服务
-    ```
-  docker-compose -f ./docker-compose-dev.yaml build
-  ```
-- 基础镜像为debian:sid-slim
-  镜像不包含nginx,仅启动web服务
-    ```
-  docker-compose -f ./docker-compose.yaml build
-  ```
-前端在本地构建,以上构建方式不包含前端
-
-## 部署
-### 使用docker部署
-- 将docker-compose.yaml 或者 docker-compose-dev.yaml 复制到自己的文件夹下
-修改编排中的volumes,更改目录映射,容器内 /app/data 为目录持久化数据所在目录。
-```shell
-docker-compose -f ./docker-compose.yaml up -d
-```
-使用IP:8080端口访问
-或者
-```shell
-docker run -itd -v ./data/:/app/data --network host --name tuonina/nginx-with-ui
-# or
-docker run -itd -v ./data/:/app/data -p8080:8080 --name tuonina/nginx-ui
-```
-
-### 本地部署
-```shell
-# 下载构建产物,解压
-
-```
-
-## 截图
-  ![实例列表](./docs/images/list.png)
-
-  ![实例信息](./docs/images/dashboard.png)
-
-  ![负载均衡](./docs/images/upstream.png)
-
-  ![虚拟主机](./docs/images/server.png)
-
-## 参考文档
-配置部分参考一下文档:
-
-- [Nginx Rewrite](https://blog.csdn.net/qq1356059950/article/details/125014248)
-
-
-## 构建部署
-以下操作进入到项目根目录执行
-### 构建
- docker-compose build 或者执行脚本sh build.sh
-
-## nginx-ui优化点
-- [x] nginx实例列表界面,添加完实例之后,弹窗没有关闭,且没有自动刷新当前界面
-- [x] server或者location的rewrite是否配置的判定问题
-- [x] nginx 实例设置时,将需要的目录文件创建好
-- [x] 新增虚拟主机或者其它新增界面,无法重置当前的表单,需要结合planning-tools,增加重置功能
-- [x] 证书管理,将证书信息保存到数据库,方便做nginx服务前移,不能直接写文件,可以增加从文件夹同步的功能
-- [x] 后端docker启动时,默认启动本地的NGINX,docker镜像问题
-- [ ] 很多界面的默认值问题,优化初始值
-- [x] nginx.conf默认值问题,每次容器重启都会被重置 
-- [x] 增强证书管理:添加时间,有效期,域名
-- [x] 增加一些快捷按钮,比如转发真实IP,支持websocket,跨域设置等
-- [x] 考虑是否增加静态站点的文件上传功能
-- [ ] 考虑多租户的功能,目前前端使用的是nginx auth的授权认证,该方式是否能传递用户id作为查询数据的条件
-- [ ] 考虑增加jwt,basic授权
-
-### 2023-07-06
-- [ ] [ngx_http_auth_request_module](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html)
-    鉴权模块的实现
-
-## 更新日志
-- 20230710:修复return 语句未渲染的问题
-- 20230719: 修复return语句在代理或者静态站点的情况下依然渲染的问题
-
-## git代理
-git config --global http.proxy 127.0.0.1 1234
-git config --global https.proxy  127.0.0.1 1234
-git config --global --unset http.proxy
-git config --global --unset https.proxy
+# nginx 可视化界面
+项目的主要功能未nginx的配置管理,通过可视化的界面去配置nginx,所有的配置渲染逻辑都在前端进行,通过后台服务渲染到部署nginx的服务器上;\
+由于nginx的配置实在是太多了,只是可视化了部分常用的功能;\
+可以用于开发环境,需要经常变动一些配置信息的场景.
+
+- 项目的web前端基于react开发,使用vite构建工具;
+- 后端使用golang语言开发(菜鸟学习中)
+
+## 构建
+项目构建基于docker-compose, 分为两种情况
+- 基础镜像为nginx
+  容器自带nginx,启用web服务
+    ```
+  docker-compose -f ./docker-compose-dev.yaml build
+  ```
+- 基础镜像为debian:sid-slim
+  镜像不包含nginx,仅启动web服务
+    ```
+  docker-compose -f ./docker-compose.yaml build
+  ```
+前端在本地构建,以上构建方式不包含前端
+
+## 部署
+### 使用docker部署
+- 将docker-compose.yaml 或者 docker-compose-dev.yaml 复制到自己的文件夹下
+修改编排中的volumes,更改目录映射,容器内 /app/data 为目录持久化数据所在目录。
+```shell
+docker-compose -f ./docker-compose.yaml up -d
+```
+使用IP:8080端口访问
+或者
+```shell
+docker run -itd -v ./data/:/app/data --network host --name tuonina/nginx-with-ui
+# or
+docker run -itd -v ./data/:/app/data -p8080:8080 --name tuonina/nginx-ui
+```
+
+### 本地部署
+```shell
+# 下载构建产物,解压
+
+```
+
+## 截图
+  ![实例列表](./docs/images/list.png)
+
+  ![实例信息](./docs/images/dashboard.png)
+
+  ![负载均衡](./docs/images/upstream.png)
+
+  ![虚拟主机](./docs/images/server.png)
+
+## 参考文档
+配置部分参考一下文档:
+
+- [Nginx Rewrite](https://blog.csdn.net/qq1356059950/article/details/125014248)
+
+
+## 构建部署
+以下操作进入到项目根目录执行
+### 构建
+ docker-compose build 或者执行脚本sh build.sh
+
+
+
+## nginx-ui优化点
+- [x] nginx实例列表界面,添加完实例之后,弹窗没有关闭,且没有自动刷新当前界面
+- [x] server或者location的rewrite是否配置的判定问题
+- [x] nginx 实例设置时,将需要的目录文件创建好
+- [x] 新增虚拟主机或者其它新增界面,无法重置当前的表单,需要结合planning-tools,增加重置功能
+- [x] 证书管理,将证书信息保存到数据库,方便做nginx服务前移,不能直接写文件,可以增加从文件夹同步的功能
+- [x] 后端docker启动时,默认启动本地的NGINX,docker镜像问题
+- [ ] 很多界面的默认值问题,优化初始值
+- [x] nginx.conf默认值问题,每次容器重启都会被重置 
+- [x] 增强证书管理:添加时间,有效期,域名
+- [x] 增加一些快捷按钮,比如转发真实IP,支持websocket,跨域设置等
+- [x] 考虑是否增加静态站点的文件上传功能
+- [ ] 考虑多租户的功能,目前前端使用的是nginx auth的授权认证,该方式是否能传递用户id作为查询数据的条件
+- [ ] 考虑增加jwt,basic授权
+
+### 2023-07-06
+- [ ] [ngx_http_auth_request_module](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html)
+    鉴权模块的实现
+
+## 更新日志
+- 20230710:修复return 语句未渲染的问题
+- 20230719: 修复return语句在代理或者静态站点的情况下依然渲染的问题
+
+## git代理
+git config --global http.proxy 127.0.0.1 1234
+git config --global https.proxy  127.0.0.1 1234
+git config --global --unset http.proxy
+git config --global --unset https.proxy

+ 3 - 3
build-nginx-with-ui.sh

@@ -1,3 +1,3 @@
-#/usr/bin/sh
-export DOCKER_BUILDKIT=1
-docker build . -f docker/Dockerfile-with-nginx  -t tuonina/nginx-with-ui:latest
+#/usr/bin/sh
+export DOCKER_BUILDKIT=1
+docker build . -f docker/Dockerfile-with-nginx  -t tuonina/nginx-with-ui:latest

+ 18 - 18
build-server.sh

@@ -1,18 +1,18 @@
-#!/bin/bash
-CURRENT_DIR=$(cd $(dirname $0); pwd)
-
-mkdir -p ./local
-mkdir -p ./local/data/db
-cp -rf ./server/conf ./local
-cp -rf ./server/static ./local
-
-cd ./server
-export GOODS=linux
-export GOARCH=amd64
-go build -o ../local/server
-cd $CURRENT_DIR
-cp -rf ./dist/* ./local/static/web/
-rm -rf ./local/static/web/config.js
-chmod +x ./local/server
-
-tar -czf nginx-ui.tar.gz ./local
+#!/bin/bash
+CURRENT_DIR=$(cd $(dirname $0); pwd)
+
+mkdir -p ./local
+mkdir -p ./local/data/db
+cp -rf ./server/conf ./local
+cp -rf ./server/static ./local
+
+cd ./server
+export GOODS=linux
+export GOARCH=amd64
+go build -o ../local/server
+cd $CURRENT_DIR
+cp -rf ./dist/* ./local/static/web/
+rm -rf ./local/static/web/config.js
+chmod +x ./local/server
+
+tar -czf nginx-ui.tar.gz ./local

+ 3 - 3
build.sh

@@ -1,3 +1,3 @@
-#/usr/bin/sh
-export DOCKER_BUILDKIT=1
-docker build . -f docker/Dockerfile  -t tuonina/nginx-ui:latest
+#/usr/bin/sh
+export DOCKER_BUILDKIT=1
+docker build . -f docker/Dockerfile  -t tuonina/nginx-ui:latest

+ 6 - 6
dev.sh

@@ -1,6 +1,6 @@
-#/usr/bin/sh
-export DOCKER_BUILDKIT=1
-docker-compose -f ./docker-compose-dev.yaml build
-docker-compose -f ./docker-compose-dev.yaml up -d
-
-
+#/usr/bin/sh
+export DOCKER_BUILDKIT=1
+docker-compose -f ./docker-compose-dev.yaml build
+docker-compose -f ./docker-compose-dev.yaml up -d
+
+

File diff suppressed because it is too large
+ 0 - 8
dist/assets/index-96b4a255.js


File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-976dda2b.css


+ 3 - 3
dist/config.js

@@ -1,4 +1,4 @@
-// config.js
-window.CONFIG = {
-    baseApi: '/api/nginx-ui/api'
+// config.js
+window.CONFIG = {
+    baseApi: '/api/nginx-ui/api'
 }

+ 45 - 45
dist/index.html

@@ -1,46 +1,46 @@
-<!DOCTYPE html><html lang="en"><head>
-    <meta charset="UTF-8">
-    <link rel="icon" type="image/svg+xml" href="/nginx-ui/vite.svg">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>NginxUI</title>
-    <script type="application/javascript" src="./config.js"></script>
-    <script crossorigin="">import('/nginx-ui/assets/index-96b4a255.js').finally(() => {
-            
-    const qiankunLifeCycle = window.moudleQiankunAppLifeCycles && window.moudleQiankunAppLifeCycles['nginx-ui'];
-    if (qiankunLifeCycle) {
-      window.proxy.vitemount((props) => qiankunLifeCycle.mount(props));
-      window.proxy.viteunmount((props) => qiankunLifeCycle.unmount(props));
-      window.proxy.vitebootstrap(() => qiankunLifeCycle.bootstrap());
-      window.proxy.viteupdate((props) => qiankunLifeCycle.update(props));
-    }
-  
-          })</script>
-    <link rel="stylesheet" href="/nginx-ui/assets/index-976dda2b.css">
-  </head>
-  <body>
-    <div id="nginx_ui_root"></div>
-    
-  
-
-<script>
-  const createDeffer = (hookName) => {
-    const d = new Promise((resolve, reject) => {
-      window.proxy && (window.proxy[`vite${hookName}`] = resolve)
-    })
-    return props => d.then(fn => fn(props));
-  }
-  const bootstrap = createDeffer('bootstrap');
-  const mount = createDeffer('mount');
-  const unmount = createDeffer('unmount');
-  const update = createDeffer('update');
-
-  ;(global => {
-    global.qiankunName = 'nginx-ui';
-    global['nginx-ui'] = {
-      bootstrap,
-      mount,
-      unmount,
-      update
-    };
-  })(window);
+<!DOCTYPE html><html lang="en"><head>
+    <meta charset="UTF-8">
+    <link rel="icon" type="image/svg+xml" href="/nginx-ui/vite.svg">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>NginxUI</title>
+    <script type="application/javascript" src="./config.js"></script>
+    <script crossorigin="">import('/nginx-ui/assets/index-96b4a255.js').finally(() => {
+            
+    const qiankunLifeCycle = window.moudleQiankunAppLifeCycles && window.moudleQiankunAppLifeCycles['nginx-ui'];
+    if (qiankunLifeCycle) {
+      window.proxy.vitemount((props) => qiankunLifeCycle.mount(props));
+      window.proxy.viteunmount((props) => qiankunLifeCycle.unmount(props));
+      window.proxy.vitebootstrap(() => qiankunLifeCycle.bootstrap());
+      window.proxy.viteupdate((props) => qiankunLifeCycle.update(props));
+    }
+  
+          })</script>
+    <link rel="stylesheet" href="/nginx-ui/assets/index-976dda2b.css">
+  </head>
+  <body>
+    <div id="nginx_ui_root"></div>
+    
+  
+
+<script>
+  const createDeffer = (hookName) => {
+    const d = new Promise((resolve, reject) => {
+      window.proxy && (window.proxy[`vite${hookName}`] = resolve)
+    })
+    return props => d.then(fn => fn(props));
+  }
+  const bootstrap = createDeffer('bootstrap');
+  const mount = createDeffer('mount');
+  const unmount = createDeffer('unmount');
+  const update = createDeffer('update');
+
+  ;(global => {
+    global.qiankunName = 'nginx-ui';
+    global['nginx-ui'] = {
+      bootstrap,
+      mount,
+      unmount,
+      update
+    };
+  })(window);
 </script></body></html>

+ 22 - 22
docker-compose-dev.yaml

@@ -1,22 +1,22 @@
-## 以nginx为基础镜像,方便调试,或者直接使用nginx容器
-
-version: "3"
-
-services:
-  nginx-with-ui:
-    build:
-      context: ./
-      dockerfile: docker/Dockerfile-with-nginx
-    environment:
-      - DOCKER_BUILDKIT=1
-    image: tuonina/nginx-with-ui
-    restart: always
-    container_name: nginx-with-ui
-    ports:
-      - 8080:8080
-      - 9090:9090
-      - 9080:80
-      - 9443:443
-#    network_mode: host
-    volumes:
-      - ./docker/data:/app/data
+## 以nginx为基础镜像,方便调试,或者直接使用nginx容器
+
+version: "3"
+
+services:
+  nginx-with-ui:
+    build:
+      context: ./
+      dockerfile: docker/Dockerfile-with-nginx
+    environment:
+      - DOCKER_BUILDKIT=1
+    image: tuonina/nginx-with-ui
+    restart: always
+    container_name: nginx-with-ui
+    ports:
+      - 8080:8080
+      - 9090:9090
+      - 9080:80
+      - 9443:443
+#    network_mode: host
+    volumes:
+      - ./docker/data:/app/data

+ 16 - 16
docker-compose.yaml

@@ -1,16 +1,16 @@
-version: "3"
-
-services:
-  nginx-ui:
-    build:
-      context: ./
-      dockerfile: docker/Dockerfile
-    environment:
-      - DOCKER_BUILDKIT=1
-    image: tuonina/nginx-ui
-    restart: always
-    container_name: nginx-ui
-    ports:
-      - 8080:8080
-    volumes:
-      - ./data:/app/data
+version: "3"
+
+services:
+  nginx-ui:
+    build:
+      context: ./
+      dockerfile: docker/Dockerfile
+    environment:
+      - DOCKER_BUILDKIT=1
+    image: tuonina/nginx-ui
+    restart: always
+    container_name: nginx-ui
+    ports:
+      - 8080:8080
+    volumes:
+      - ./data:/app/data

+ 29 - 29
docker/Dockerfile

@@ -1,29 +1,29 @@
-FROM golang:1.20 AS builder
-WORKDIR /app
-COPY server .
-ARG GOODS=linux
-ARG GOARCH=amd64
-
-RUN go env -w GOPROXY=https://goproxy.cn,direct
-
-RUN --mount=type=cache,target=/go --mount=type=cache,target=/root/.cache/go-build\
-    GOODS=${GOODS} GOARCH=${GOARCH}  go build -o /app/app
-
-FROM debian:sid-slim
-
-WORKDIR /app
-COPY --from=builder /app/app /app
-COPY server/conf /app/conf
-COPY server/data  /app/data
-#COPY ../server/static  /app/static
-COPY dist  /app/static/web
-
-RUN chmod +x /app/app
-
-RUN rm -rf /app/static/web/config.js
-
-#RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
-#RUN sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
-#RUN echo "deb http://mirrors.ustc.edu.cn/debian sid main" >> /etc/apt/sources.list
-
-ENTRYPOINT ["/app/app"]
+FROM golang:1.20 AS builder
+WORKDIR /app
+COPY server .
+ARG GOODS=linux
+ARG GOARCH=amd64
+
+RUN go env -w GOPROXY=https://goproxy.cn,direct
+
+RUN --mount=type=cache,target=/go --mount=type=cache,target=/root/.cache/go-build\
+    GOODS=${GOODS} GOARCH=${GOARCH}  go build -o /app/app
+
+FROM debian:sid-slim
+
+WORKDIR /app
+COPY --from=builder /app/app /app
+COPY server/conf /app/conf
+COPY server/data  /app/data
+#COPY ../server/static  /app/static
+COPY dist  /app/static/web
+
+RUN chmod +x /app/app
+
+RUN rm -rf /app/static/web/config.js
+
+#RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
+#RUN sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
+#RUN echo "deb http://mirrors.ustc.edu.cn/debian sid main" >> /etc/apt/sources.list
+
+ENTRYPOINT ["/app/app"]

+ 30 - 30
docker/Dockerfile-with-nginx

@@ -1,30 +1,30 @@
-FROM golang:1.20 AS builder
-WORKDIR /app
-COPY server .
-ARG GOODS=linux
-ARG GOARCH=amd64
-
-RUN go env -w GOPROXY=https://goproxy.cn,direct
-
-RUN --mount=type=cache,target=/go --mount=type=cache,target=/root/.cache/go-build\
-    GOODS=${GOODS} GOARCH=${GOARCH}  go build -o /app/app
-
-FROM nginx:1.25.1
-
-WORKDIR /app
-COPY --from=builder /app/app /app
-COPY server/conf /app/conf
-COPY server/data  /app/data
-#COPY ../server/static  /app/static
-COPY dist  /app/static/web
-COPY docker/entrypoint.sh /entrypoint.sh
-
-RUN chmod +x /entrypoint.sh /app/app
-
-RUN rm -rf /app/static/web/config.js
-RUN rm -rf /etc/nginx/conf.d
-#RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
-#RUN sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
-#RUN echo "deb http://mirrors.ustc.edu.cn/debian sid main" >> /etc/apt/sources.list
-
-ENTRYPOINT ["/entrypoint.sh"]
+FROM golang:1.20 AS builder
+WORKDIR /app
+COPY server .
+ARG GOODS=linux
+ARG GOARCH=amd64
+
+RUN go env -w GOPROXY=https://goproxy.cn,direct
+
+RUN --mount=type=cache,target=/go --mount=type=cache,target=/root/.cache/go-build\
+    GOODS=${GOODS} GOARCH=${GOARCH}  go build -o /app/app
+
+FROM nginx:1.25.1
+
+WORKDIR /app
+COPY --from=builder /app/app /app
+COPY server/conf /app/conf
+COPY server/data  /app/data
+#COPY ../server/static  /app/static
+COPY dist  /app/static/web
+COPY docker/entrypoint.sh /entrypoint.sh
+
+RUN chmod +x /entrypoint.sh /app/app
+
+RUN rm -rf /app/static/web/config.js
+RUN rm -rf /etc/nginx/conf.d
+#RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
+#RUN sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
+#RUN echo "deb http://mirrors.ustc.edu.cn/debian sid main" >> /etc/apt/sources.list
+
+ENTRYPOINT ["/entrypoint.sh"]

+ 15 - 15
docker/entrypoint.sh

@@ -1,15 +1,15 @@
-#!/bin/sh
-
-
-dataDir=${DATADIR:-/app/data}
-echo "dataDir: $dataDir"
-
-if [ -f "$dataDir/nginx.conf" ];then
-  if [ -f /etc/nginx/nginx.conf ]; then
-      mv -f /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
-  fi
-  ln -s $dataDir/nginx.conf  /etc/nginx/nginx.conf
-fi
-nginx -g "daemon on;"
-## 启用 nginx-ui的服务
-/app/app
+#!/bin/sh
+
+
+dataDir=${DATADIR:-/app/data}
+echo "dataDir: $dataDir"
+
+if [ -f "$dataDir/nginx.conf" ];then
+  if [ -f /etc/nginx/nginx.conf ]; then
+      mv -f /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
+  fi
+  ln -s $dataDir/nginx.conf  /etc/nginx/nginx.conf
+fi
+nginx -g "daemon on;"
+## 启用 nginx-ui的服务
+/app/app

+ 14 - 14
index.html

@@ -1,14 +1,14 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>NginxUI</title>
-    <script type="application/javascript" src="./config.js"></script>
-  </head>
-  <body>
-    <div id="nginx_ui_root"></div>
-    <script type="module" src="/src/main.tsx"></script>
-  </body>
-</html>
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>NginxUI</title>
+    <script type="application/javascript" src="./config.js"></script>
+  </head>
+  <body>
+    <div id="nginx_ui_root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 64 - 64
package.json

@@ -1,64 +1,64 @@
-{
-  "name": "nginx-ui-react",
-  "private": false,
-  "version": "0.1.0",
-  "license": "MIT",
-  "author": {
-    "email": "976056042@qq.com",
-    "name": "tuonina"
-  },
-  "type": "module",
-  "scripts": {
-    "dev": "vite",
-    "build": "tsc && vite build --base=/nginx-ui/",
-    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
-    "preview": "vite preview"
-  },
-  "dependencies": {
-    "@ant-design/icons": "^5.1.4",
-    "@mdx-js/mdx": "^2.0.0-rc.2",
-    "@mdx-js/react": "^2.3.0",
-    "@mdx-js/rollup": "^2.3.0",
-    "@reduxjs/toolkit": "^1.9.5",
-    "antd": "4.x",
-    "artt-template": "^4.13.6",
-    "classnames": "^2.3.2",
-    "events": "^3.3.0",
-    "history": "^5.3.0",
-    "install": "^0.13.0",
-    "jszip": "^3.10.1",
-    "less": "^4.1.3",
-    "lodash": "^4.17.21",
-    "npm": "^9.8.0",
-    "planning-tools": "^0.1.1",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0",
-    "react-redux": "^8.1.1",
-    "react-router": "^6.14.0",
-    "react-router-dom": "^6.14.0",
-    "redux-persist": "^6.0.0"
-  },
-  "devDependencies": {
-    "@types/events": "^3.0.0",
-    "@types/lodash": "^4.14.195",
-    "@types/react": "^18.0.37",
-    "@types/react-dom": "^18.0.11",
-    "@typescript-eslint/eslint-plugin": "^5.59.0",
-    "@typescript-eslint/parser": "^5.59.0",
-    "@vitejs/plugin-react-swc": "^3.0.0",
-    "consola": "^3.1.0",
-    "eslint": "^8.38.0",
-    "eslint-plugin-react-hooks": "^4.6.0",
-    "eslint-plugin-react-refresh": "^0.3.4",
-    "typescript": "^5.0.2",
-    "vite": "^4.3.9",
-    "vite-plugin-md": "^0.21.5",
-    "vite-plugin-mdx": "^3.5.11",
-    "vite-plugin-qiankun": "^1.0.15",
-    "vite-plugin-require-transform": "^1.0.20",
-    "vite-plugin-style-import": "^2.0.0"
-  },
-  "peerDependencies": {
-    "artt-template": "^4.13.6"
-  }
-}
+{
+  "name": "nginx-ui-react",
+  "private": false,
+  "version": "0.1.0",
+  "license": "MIT",
+  "author": {
+    "email": "976056042@qq.com",
+    "name": "tuonina"
+  },
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build --base=/nginx-ui/",
+    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@ant-design/icons": "^5.1.4",
+    "@mdx-js/mdx": "^2.0.0-rc.2",
+    "@mdx-js/react": "^2.3.0",
+    "@mdx-js/rollup": "^2.3.0",
+    "@reduxjs/toolkit": "^1.9.5",
+    "antd": "4.x",
+    "artt-template": "^4.13.6",
+    "classnames": "^2.3.2",
+    "events": "^3.3.0",
+    "history": "^5.3.0",
+    "install": "^0.13.0",
+    "jszip": "^3.10.1",
+    "less": "^4.1.3",
+    "lodash": "^4.17.21",
+    "npm": "^9.8.0",
+    "planning-tools": "^0.1.1",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-redux": "^8.1.1",
+    "react-router": "^6.14.0",
+    "react-router-dom": "^6.14.0",
+    "redux-persist": "^6.0.0"
+  },
+  "devDependencies": {
+    "@types/events": "^3.0.0",
+    "@types/lodash": "^4.14.195",
+    "@types/react": "^18.0.37",
+    "@types/react-dom": "^18.0.11",
+    "@typescript-eslint/eslint-plugin": "^5.59.0",
+    "@typescript-eslint/parser": "^5.59.0",
+    "@vitejs/plugin-react-swc": "^3.0.0",
+    "consola": "^3.1.0",
+    "eslint": "^8.38.0",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-react-refresh": "^0.3.4",
+    "typescript": "^5.0.2",
+    "vite": "^4.3.9",
+    "vite-plugin-md": "^0.21.5",
+    "vite-plugin-mdx": "^3.5.11",
+    "vite-plugin-qiankun": "^1.0.15",
+    "vite-plugin-require-transform": "^1.0.20",
+    "vite-plugin-style-import": "^2.0.0"
+  },
+  "peerDependencies": {
+    "artt-template": "^4.13.6"
+  }
+}

+ 3 - 3
public/config.js

@@ -1,4 +1,4 @@
-// config.js
-window.CONFIG = {
-    baseApi: '/api/nginx-ui/api'
+// config.js
+window.CONFIG = {
+    baseApi: '/api/nginx-ui/api'
 }

+ 19 - 3
server/conf/app.conf

@@ -4,18 +4,34 @@ runmode = dev
 copyrequestbody = true
 
 baseApi = /nginx-ui/api
+contextpath = /nginx-ui
 
 datadir = ./data
 dbdir = ./data/db
 nginxPath = /usr/sbin/nginx
 nginxDir = /etc/nginx
 
+admin_password =
+reset_admin_password =
+
 sessionon = true
 sessionprovider = file
 sessionname = nginx_session
 sessiongcmaxlifetime = 7200
 sessionproviderconfig = "./data/sessions"
 
-thirdsessionenable = true
-thirdsessionname = tq.session
-thirdsessioncheckurl = http://10.10.0.1:20003/session/check
+thirdsessionenable = false
+thirdsessionname =
+thirdsessioncheckurl =
+
+
+oauth2_client_id =
+oauth2_client_secret =
+oauth2_authorize_endpoint =
+oauth2_token_endpoint =
+oauth2_redirect_uri =
+oauth2_scopes = ""
+oauth2_userinfo =
+oauth2_enable = false
+
+include "app.local.conf"

+ 111 - 40
server/config/config.go

@@ -1,40 +1,111 @@
-package config
-
-import (
-	"github.com/astaxie/beego"
-	"github.com/astaxie/beego/logs"
-	"os"
-	"server/utils"
-)
-
-type AppConfig struct {
-	BaseApi string
-	DataDir string
-}
-
-var Config = &AppConfig{}
-
-func GetDataDir() string {
-	return Config.DataDir
-}
-
-func init() {
-	// 需要和前端配置好
-	baseApi := beego.AppConfig.String("baseApi")
-	if baseApi == "" {
-		baseApi = "/ngx"
-		err := beego.AppConfig.Set("baseApi", baseApi)
-		if err != nil {
-			logs.Info("init set baseApi", err)
-		}
-	}
-	Config.BaseApi = baseApi
-	Config.DataDir = beego.AppConfig.String("datadir")
-	if exist := utils.IsExist(Config.DataDir); exist == false {
-		err := os.MkdirAll(Config.DataDir, 0777)
-		logs.Warn("create data dir fail", err)
-		if err != nil {
-			panic(err)
-		}
-	}
-}
+package config
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"github.com/astaxie/beego"
+	"github.com/astaxie/beego/logs"
+	"github.com/astaxie/beego/orm"
+	"golang.org/x/oauth2"
+	"os"
+	"server/models"
+	"server/utils"
+	"strings"
+)
+
+type AppConfig struct {
+	BaseApi     string
+	DataDir     string
+	ContextPath string
+}
+
+type CompleteOauth2Config struct {
+	*oauth2.Config
+	Userinfo string
+	Enable   bool
+}
+
+var OauthConfig = &CompleteOauth2Config{
+	Enable: false,
+}
+
+var Config = &AppConfig{}
+
+func GetDataDir() string {
+	return Config.DataDir
+}
+
+func init() {
+	// 需要和前端配置好
+	baseApi := beego.AppConfig.String("baseApi")
+	if baseApi == "" {
+		baseApi = "/ngx"
+		err := beego.AppConfig.Set("baseApi", baseApi)
+		if err != nil {
+			logs.Info("init set baseApi", err)
+		}
+	}
+	baseApi = strings.TrimSuffix(baseApi, "/")
+	Config.ContextPath = beego.AppConfig.DefaultString("contextpath", "")
+	Config.ContextPath = strings.TrimSuffix(Config.ContextPath, "/")
+	Config.BaseApi = baseApi
+	Config.DataDir = beego.AppConfig.String("datadir")
+	if exist := utils.IsExist(Config.DataDir); exist == false {
+		err := os.MkdirAll(Config.DataDir, 0777)
+		logs.Warn("create data dir fail", err)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	OauthConfig.ClientID = beego.AppConfig.DefaultString("oauth2_client_id", "")
+	OauthConfig.ClientSecret = beego.AppConfig.DefaultString("oauth2_client_secret", "")
+
+	authorizeEndpoint := beego.AppConfig.DefaultString("oauth2_authorize_endpoint", "")
+	tokenEndpoint := beego.AppConfig.DefaultString("oauth2_token_endpoint", "")
+
+	OauthConfig.Endpoint = oauth2.Endpoint{
+		AuthURL:   authorizeEndpoint,
+		TokenURL:  tokenEndpoint,
+		AuthStyle: 0,
+	}
+	OauthConfig.RedirectURL = beego.AppConfig.DefaultString("oauth2_redirect_uri", "")
+	OauthConfig.Scopes = beego.AppConfig.DefaultStrings("oauth2_scopes", []string{})
+	OauthConfig.Userinfo = beego.AppConfig.DefaultString("oauth2_userinfo", "")
+	OauthConfig.Enable = beego.AppConfig.DefaultBool("oauth2_enable", false)
+}
+
+func InitAdmin() {
+	o := orm.NewOrm()
+	reset := beego.AppConfig.DefaultBool("reset_admin_password", false)
+	admin := models.User{Account: "admin"}
+	err := o.Read(&admin, "Account")
+	if err != nil && !reset {
+		return
+	}
+	password := beego.AppConfig.DefaultString("admin_password", randPassword(10))
+	admin.Password = utils.GetSHA256HashCode(password)
+	admin.Remark = "admin"
+	admin.Roles = "ADMIN"
+	if admin.Id > 0 {
+		_, err = o.Update(&admin)
+	} else {
+		_, err = o.Insert(&admin)
+	}
+	if err != nil {
+		logs.Warn("insert or update admin fail", err)
+	} else {
+		logs.Warn(fmt.Sprintf("admin password is:  %s", password))
+	}
+
+}
+
+func randPassword(n int) string {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	if err != nil {
+		return "123456"
+	}
+	return base64.StdEncoding.EncodeToString(b)
+}

+ 104 - 85
server/controllers/default.go

@@ -1,85 +1,104 @@
-package controllers
-
-import (
-	"github.com/astaxie/beego"
-	"github.com/astaxie/beego/logs"
-	"strconv"
-)
-
-type RespData struct {
-	Code int         `json:"code"`
-	Msg  string      `json:"msg"`
-	Data interface{} `json:"data"`
-}
-
-type BaseController struct {
-	beego.Controller
-	jsonData *RespData
-	// json real data
-	respData map[string]any
-}
-
-func (c *BaseController) json() {
-	c.checkJsonData()
-	if c.respData != nil {
-		c.jsonData.Data = c.respData
-	}
-	c.Data["json"] = c.jsonData
-	c.ServeJSON()
-}
-
-func (c *BaseController) ErrorJson(error error) {
-	c.setCode(-1).setMsg(error.Error()).json()
-}
-
-func (c *BaseController) checkJsonData() *BaseController {
-	data := c.jsonData
-	if data == nil {
-		data = &RespData{
-			Code: 0,
-			Msg:  "success",
-			Data: nil,
-		}
-		c.jsonData = data
-	}
-	return c
-}
-
-func (c *BaseController) setData(v interface{}) *BaseController {
-	c.checkJsonData()
-	c.jsonData.Data = v
-	return c
-}
-
-func (c *BaseController) addRespData(k string, v interface{}) *BaseController {
-	c.checkJsonData()
-	if c.respData == nil {
-		c.respData = map[string]any{}
-	}
-	c.respData[k] = v
-	return c
-}
-
-func (c *BaseController) setCode(code int) *BaseController {
-	c.checkJsonData()
-	c.jsonData.Code = code
-	return c
-}
-func (c *BaseController) setMsg(msg string) *BaseController {
-	c.checkJsonData()
-	c.jsonData.Msg = msg
-	return c
-}
-
-func (c *BaseController) getParam(k string) string {
-	return c.Ctx.Input.Param(k)
-}
-func (c *BaseController) getIntParam(k string) (int, error) {
-	idStr := c.Ctx.Input.Param(k)
-	id, err := strconv.Atoi(idStr)
-	logs.Info("id", id)
-	if err != nil {
-		return 0, err
-	}
-	return id, nil
-}
+package controllers
+
+import (
+	"github.com/astaxie/beego"
+	"github.com/astaxie/beego/logs"
+	"server/middleware"
+	"server/models"
+	"strconv"
+)
+
+type RespData struct {
+	Code int         `json:"code"`
+	Msg  string      `json:"msg"`
+	Data interface{} `json:"data"`
+}
+
+type BaseController struct {
+	beego.Controller
+	jsonData *RespData
+	// json real data
+	respData map[string]any
+}
+
+func (c *BaseController) json() {
+	c.checkJsonData()
+	if c.respData != nil {
+		c.jsonData.Data = c.respData
+	}
+	c.Data["json"] = c.jsonData
+	c.ServeJSON()
+}
+
+func (c *BaseController) ErrorJson(error error) {
+	c.setCode(-1).setMsg(error.Error()).json()
+}
+
+func (c *BaseController) checkJsonData() *BaseController {
+	data := c.jsonData
+	if data == nil {
+		data = &RespData{
+			Code: 0,
+			Msg:  "success",
+			Data: nil,
+		}
+		c.jsonData = data
+	}
+	return c
+}
+
+func (c *BaseController) setData(v interface{}) *BaseController {
+	c.checkJsonData()
+	c.jsonData.Data = v
+	return c
+}
+
+func (c *BaseController) addRespData(k string, v interface{}) *BaseController {
+	c.checkJsonData()
+	if c.respData == nil {
+		c.respData = map[string]any{}
+	}
+	c.respData[k] = v
+	return c
+}
+
+func (c *BaseController) setCode(code int) *BaseController {
+	c.checkJsonData()
+	c.jsonData.Code = code
+	return c
+}
+func (c *BaseController) setMsg(msg string) *BaseController {
+	c.checkJsonData()
+	c.jsonData.Msg = msg
+	return c
+}
+
+func (c *BaseController) getParam(k string) string {
+	return c.Ctx.Input.Param(k)
+}
+func (c *BaseController) getIntParam(k string) (int, error) {
+	idStr := c.Ctx.Input.Param(k)
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}
+
+func (c *BaseController) GetUser(required bool) *models.User {
+	data := c.GetSession("user")
+	if data == nil && required {
+		middleware.WriteForbidden(c.Ctx.ResponseWriter)
+		return nil
+	}
+	if data == nil {
+		return nil
+	}
+	user := data.(models.User)
+	return &user
+}
+
+func (c *BaseController) RequiredUser() *models.User {
+	return c.GetUser(true)
+}

+ 255 - 250
server/controllers/nginx.go

@@ -1,250 +1,255 @@
-package controllers
-
-import (
-	"encoding/json"
-	"github.com/astaxie/beego/logs"
-	"github.com/astaxie/beego/orm"
-	"server/models"
-	ngx "server/nginx"
-	"strconv"
-)
-
-type NginxController struct {
-	BaseController
-}
-
-const ReplacePassword = "******"
-
-// Get getAll
-func (c *NginxController) Get() {
-	o := orm.NewOrm()
-	qs := o.QueryTable("nginx")
-	var list []*models.Nginx
-	_, err := qs.All(&list)
-	for i := range list {
-		item := list[i]
-		if item.Password != "" {
-			item.Password = ReplacePassword
-		}
-	}
-
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	} else {
-		c.setData(list).json()
-	}
-}
-
-// Post add nginx instance
-func (c *NginxController) Post() {
-	var nginx models.Nginx
-	err := json.Unmarshal(c.Ctx.Input.RequestBody, &nginx)
-	if err != nil {
-		logs.Error(err, string(c.Ctx.Input.RequestBody))
-		c.ErrorJson(err)
-		return
-	}
-	nginx.Check()
-	o := orm.NewOrm()
-	var saveErr error
-	if nginx.Id == 0 {
-		_, err = o.Insert(&nginx)
-		saveErr = err
-	} else {
-		tmp := models.Nginx{
-			Id: nginx.Id,
-		}
-		err = o.Read(&tmp)
-		if err != nil {
-			c.ErrorJson(err)
-			return
-		}
-		if nginx.Password == ReplacePassword {
-			nginx.Password = tmp.Password
-		}
-		nginx.HttpConf = tmp.HttpConf
-		_, err = o.Update(&nginx)
-		saveErr = err
-	}
-
-	if saveErr != nil {
-		c.ErrorJson(saveErr)
-		return
-	}
-	logs.Info("post", nginx)
-
-	instance := ngx.GetInstance(&nginx)
-	err = instance.Connect()
-	if err != nil {
-		c.setCode(1).setMsg(err.Error()).setData(nginx)
-		c.json()
-		return
-	}
-	out, err := instance.GetVersion()
-	if err != nil {
-		c.setCode(1).setMsg(err.Error()).setData(nginx)
-		c.json()
-		return
-	}
-	nginx.VersionInfo = out
-	_, _ = o.Update(&nginx, "VersionInfo")
-	c.setData(nginx).json()
-}
-
-// StartNginx startNginx
-func (c *NginxController) StartNginx() {
-	idStr := c.getParam(":id")
-	id, err := strconv.Atoi(idStr)
-	logs.Info("id", id)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	var nginx = models.Nginx{
-		Id: id,
-	}
-	o := orm.NewOrm()
-	err = o.Read(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	instance := ngx.GetInstance(&nginx)
-	err = instance.Start()
-	isRun, msg := instance.Status()
-	c.setData(isRun).setMsg(msg).json()
-}
-
-// StopNginx add nginx instance
-func (c *NginxController) StopNginx() {
-	idStr := c.getParam(":id")
-	id, err := strconv.Atoi(idStr)
-	logs.Info("id", id)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-
-	var nginx = models.Nginx{
-		Id: id,
-	}
-	o := orm.NewOrm()
-	err = o.Read(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	instance := ngx.GetInstance(&nginx)
-	err = instance.Stop()
-	isRun, msg := instance.Status()
-	c.setData(isRun).setMsg(msg).json()
-}
-
-// RefreshHttp nginx detail data
-func (c *NginxController) RefreshHttp() {
-	var nginx models.Nginx
-	err := json.Unmarshal(c.Ctx.Input.RequestBody, &nginx)
-	if err != nil {
-		logs.Error(err, string(c.Ctx.Input.RequestBody))
-		c.ErrorJson(err)
-		return
-	}
-
-	logs.Info("id", nginx)
-
-	o := orm.NewOrm()
-	if nginx.HttpConf != "" {
-		_, err = o.Update(&nginx, "HttpConf","HttpData")
-		if err != nil {
-			c.ErrorJson(err)
-			return
-		}
-	}
-	err = o.Read(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	ins := ngx.GetInstance(&nginx)
-	err = ins.RefreshHttp(nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	c.setData(nginx)
-	c.json()
-}
-
-// GetNginx nginx detail data
-func (c *NginxController) GetNginx() {
-	idStr := c.getParam(":id")
-	id, err := strconv.Atoi(idStr)
-	logs.Info("id", id)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	o := orm.NewOrm()
-
-	var nginx = models.Nginx{Id: id}
-	err = o.Read(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	if nginx.Password != "" {
-		nginx.Password = ReplacePassword
-	}
-	c.addRespData("nginx", nginx)
-
-	var servers []models.ServerHost
-	_, err = o.QueryTable((*models.ServerHost)(nil)).Filter("NginxId", id).All(&servers)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	c.addRespData("servers", servers)
-	c.json()
-}
-
-// DelNginx delete a instance
-func (c *NginxController) DelNginx() {
-	id, err := c.getIntParam(":id")
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	o := orm.NewOrm()
-	nginx := models.Nginx{Id: id}
-	count, err := o.Delete(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-	} else {
-		c.setData(count).json()
-	}
-}
-
-// StopNginx add nginx instance
-func (c *NginxController) StatusNginx() {
-	idStr := c.getParam(":id")
-	id, err := strconv.Atoi(idStr)
-	logs.Info("id", id)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-
-	var nginx = models.Nginx{
-		Id: id,
-	}
-	o := orm.NewOrm()
-	err = o.Read(&nginx)
-	if err != nil {
-		c.ErrorJson(err)
-		return
-	}
-	instance := ngx.GetInstance(&nginx)
-	isRun, msg := instance.Status()
-	c.setData(isRun).setMsg(msg).json()
-}
+package controllers
+
+import (
+	"encoding/json"
+	"github.com/astaxie/beego/logs"
+	"github.com/astaxie/beego/orm"
+	"server/models"
+	ngx "server/nginx"
+	"strconv"
+)
+
+type NginxController struct {
+	BaseController
+}
+
+const ReplacePassword = "******"
+
+// Get getAll
+func (c *NginxController) Get() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+
+	o := orm.NewOrm()
+	qs := o.QueryTable("nginx").Filter("Uid", user.Account)
+	var list []*models.Nginx
+	_, err := qs.All(&list)
+	for i := range list {
+		item := list[i]
+		if item.Password != "" {
+			item.Password = ReplacePassword
+		}
+	}
+
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	} else {
+		c.setData(list).json()
+	}
+}
+
+// Post add nginx instance
+func (c *NginxController) Post() {
+	var nginx models.Nginx
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &nginx)
+	if err != nil {
+		logs.Error(err, string(c.Ctx.Input.RequestBody))
+		c.ErrorJson(err)
+		return
+	}
+	nginx.Check()
+	o := orm.NewOrm()
+	var saveErr error
+	if nginx.Id == 0 {
+		_, err = o.Insert(&nginx)
+		saveErr = err
+	} else {
+		tmp := models.Nginx{
+			Id: nginx.Id,
+		}
+		err = o.Read(&tmp)
+		if err != nil {
+			c.ErrorJson(err)
+			return
+		}
+		if nginx.Password == ReplacePassword {
+			nginx.Password = tmp.Password
+		}
+		nginx.HttpConf = tmp.HttpConf
+		_, err = o.Update(&nginx)
+		saveErr = err
+	}
+
+	if saveErr != nil {
+		c.ErrorJson(saveErr)
+		return
+	}
+	logs.Info("post", nginx)
+
+	instance := ngx.GetInstance(&nginx)
+	err = instance.Connect()
+	if err != nil {
+		c.setCode(1).setMsg(err.Error()).setData(nginx)
+		c.json()
+		return
+	}
+	out, err := instance.GetVersion()
+	if err != nil {
+		c.setCode(1).setMsg(err.Error()).setData(nginx)
+		c.json()
+		return
+	}
+	nginx.VersionInfo = out
+	_, _ = o.Update(&nginx, "VersionInfo")
+	c.setData(nginx).json()
+}
+
+// StartNginx startNginx
+func (c *NginxController) StartNginx() {
+	idStr := c.getParam(":id")
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	var nginx = models.Nginx{
+		Id: id,
+	}
+	o := orm.NewOrm()
+	err = o.Read(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	instance := ngx.GetInstance(&nginx)
+	err = instance.Start()
+	isRun, msg := instance.Status()
+	c.setData(isRun).setMsg(msg).json()
+}
+
+// StopNginx add nginx instance
+func (c *NginxController) StopNginx() {
+	idStr := c.getParam(":id")
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+
+	var nginx = models.Nginx{
+		Id: id,
+	}
+	o := orm.NewOrm()
+	err = o.Read(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	instance := ngx.GetInstance(&nginx)
+	err = instance.Stop()
+	isRun, msg := instance.Status()
+	c.setData(isRun).setMsg(msg).json()
+}
+
+// RefreshHttp nginx detail data
+func (c *NginxController) RefreshHttp() {
+	var nginx models.Nginx
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &nginx)
+	if err != nil {
+		logs.Error(err, string(c.Ctx.Input.RequestBody))
+		c.ErrorJson(err)
+		return
+	}
+
+	logs.Info("id", nginx)
+
+	o := orm.NewOrm()
+	if nginx.HttpConf != "" {
+		_, err = o.Update(&nginx, "HttpConf", "HttpData")
+		if err != nil {
+			c.ErrorJson(err)
+			return
+		}
+	}
+	err = o.Read(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	ins := ngx.GetInstance(&nginx)
+	err = ins.RefreshHttp(nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	c.setData(nginx)
+	c.json()
+}
+
+// GetNginx nginx detail data
+func (c *NginxController) GetNginx() {
+	idStr := c.getParam(":id")
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	o := orm.NewOrm()
+
+	var nginx = models.Nginx{Id: id}
+	err = o.Read(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	if nginx.Password != "" {
+		nginx.Password = ReplacePassword
+	}
+	c.addRespData("nginx", nginx)
+
+	var servers []models.ServerHost
+	_, err = o.QueryTable((*models.ServerHost)(nil)).Filter("NginxId", id).All(&servers)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	c.addRespData("servers", servers)
+	c.json()
+}
+
+// DelNginx delete a instance
+func (c *NginxController) DelNginx() {
+	id, err := c.getIntParam(":id")
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	o := orm.NewOrm()
+	nginx := models.Nginx{Id: id}
+	count, err := o.Delete(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+	} else {
+		c.setData(count).json()
+	}
+}
+
+// StopNginx add nginx instance
+func (c *NginxController) StatusNginx() {
+	idStr := c.getParam(":id")
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+
+	var nginx = models.Nginx{
+		Id: id,
+	}
+	o := orm.NewOrm()
+	err = o.Read(&nginx)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	instance := ngx.GetInstance(&nginx)
+	isRun, msg := instance.Status()
+	c.setData(isRun).setMsg(msg).json()
+}

+ 81 - 0
server/controllers/oauth2.go

@@ -0,0 +1,81 @@
+package controllers
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/astaxie/beego/logs"
+	"github.com/astaxie/beego/orm"
+	"io"
+	"server/config"
+	"server/models"
+	"server/utils"
+)
+
+type Oauth2Controller struct {
+	BaseController
+}
+
+// Get 获取oauth2.0的登录url
+func (c *Oauth2Controller) Get() {
+	state, err := utils.RandPassword(6)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	url := config.OauthConfig.AuthCodeURL(state)
+	c.addRespData("redirect_url", url).addRespData("state", state).json()
+}
+
+// Callback 用户注册
+func (c *Oauth2Controller) Callback() {
+	oauth := config.OauthConfig
+	code := c.GetString("code", "")
+	if len(code) == 0 {
+		c.setCode(-1).setMsg("登录失败(Code):code is empty").json()
+		return
+	}
+	token, err := oauth.Exchange(context.Background(), code)
+	if err != nil {
+		logs.Error("ExchangeToken", err)
+		c.setCode(-1).setMsg("登录失败(Exchange):" + err.Error()).json()
+		return
+	}
+	client := oauth.Client(context.Background(), token)
+	resp, err := client.Get(oauth.Userinfo)
+	if err != nil {
+		logs.Error("GetUserinfo", err)
+		c.setCode(-1).setMsg(fmt.Sprintf("登录失败(Userinfo):%s", err.Error())).json()
+		return
+	}
+	defer resp.Body.Close()
+	content, err := io.ReadAll(resp.Body)
+	if err != nil {
+		logs.Error("GetUserinfo Read Body", err)
+		c.setCode(-1).setMsg(fmt.Sprintf("登录失败(Userinfo):%s", err.Error())).json()
+		return
+	}
+	user := models.User{}
+	err = json.Unmarshal(content, &user)
+	if err != nil {
+		logs.Error("GetUserinfo Unmarshal", err)
+		c.setCode(-1).setMsg(fmt.Sprintf("登录失败(Unmarshal):%s", err.Error())).json()
+		return
+	}
+	if len(user.Account) == 0 {
+		c.setCode(-1).setMsg("登录失败,请确认userinfo接口返回了account字段").json()
+		return
+	}
+	o := orm.NewOrm()
+	err = o.Read(&user, "Account")
+	if err != nil {
+		_, err = o.Insert(&user)
+	}
+	user.Password = ""
+	if err != nil {
+		c.setCode(-1).setMsg(fmt.Sprintf("保存用户失败:%s", err.Error())).json()
+		return
+	}
+	c.SetSession("user", user)
+	c.setData(user).json()
+}

+ 66 - 0
server/controllers/user.go

@@ -0,0 +1,66 @@
+package controllers
+
+import (
+	"encoding/json"
+	"github.com/astaxie/beego/logs"
+	"github.com/astaxie/beego/orm"
+	"server/models"
+	"server/utils"
+)
+
+type UserController struct {
+	BaseController
+}
+
+// Login 登录
+func (c *UserController) Login() {
+	var user models.User
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
+	if err != nil {
+		logs.Error(err, string(c.Ctx.Input.RequestBody))
+		c.ErrorJson(err)
+		return
+	}
+	cipherPassword := user.Password
+	o := orm.NewOrm()
+	err = o.Read(&user, "Account")
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	encryptPassword := utils.GetSHA256HashCode(cipherPassword)
+	if encryptPassword != user.Password {
+		c.setCode(-1).setMsg("用户名或者密码不正确!").json()
+		return
+	}
+	user.Password = ""
+	c.SetSession("user", user)
+	c.setData(user).json()
+}
+
+// Register 用户注册
+func (c *UserController) Register() {
+	var user models.User
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
+	if err != nil {
+		logs.Error(err, string(c.Ctx.Input.RequestBody))
+		c.ErrorJson(err)
+		return
+	}
+	if len(user.Account) == 0 || len(user.Password) == 0 {
+		c.setCode(-1).setMsg("账号或者密码不能为空!")
+		c.json()
+		return
+	}
+	if len(user.Nickname) == 0 {
+		user.Nickname = user.Account
+	}
+	user.Password = utils.GetSHA256HashCode(user.Password)
+	o := orm.NewOrm()
+	_, err = o.Insert(&user)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	c.setMsg("注册成功!").json()
+}

BIN
server/data/db/sqlite.db


BIN
server/data/sessions/4/0/40000f9d991d9b84f2cf3d7778d41f75


BIN
server/data/sessions/7/d/7d52340d3b7d72782a0e8a8c9359e913 → server/data/sessions/d/c/dcab0e00bab02c9ea3c863a59d2bb126


+ 8 - 4
server/go.mod

@@ -10,7 +10,7 @@ require (
 	github.com/mholt/archiver/v4 v4.0.0-alpha.8
 	github.com/pkg/sftp v1.13.5
 	github.com/smartystreets/goconvey v1.6.4
-	golang.org/x/crypto v0.10.0
+	golang.org/x/crypto v0.11.0
 )
 
 require (
@@ -21,6 +21,7 @@ require (
 	github.com/connesc/cipherio v0.2.1 // indirect
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/go-redis/redis/v7 v7.4.0 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
@@ -37,9 +38,12 @@ require (
 	github.com/therootcompany/xz v1.0.1 // indirect
 	github.com/ulikunitz/xz v0.5.10 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
-	golang.org/x/net v0.10.0 // indirect
-	golang.org/x/sys v0.9.0 // indirect
-	golang.org/x/text v0.10.0 // indirect
+	golang.org/x/net v0.12.0 // indirect
+	golang.org/x/oauth2 v0.10.0 // indirect
+	golang.org/x/sys v0.10.0 // indirect
+	golang.org/x/text v0.11.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/yaml.v2 v2.2.8 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 21 - 0
server/go.sum

@@ -80,6 +80,9 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -90,6 +93,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -188,6 +192,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
 golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -232,11 +238,15 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -264,8 +274,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
+golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -273,6 +286,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
 golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -317,6 +332,8 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -337,6 +354,10 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=

+ 2 - 2
server/main.go

@@ -4,6 +4,7 @@ import (
 	"encoding/gob"
 	"github.com/astaxie/beego"
 	_ "github.com/beego/beego/v2/server/web/session/redis"
+	"server/config"
 	_ "server/config"
 	"server/db"
 	"server/models"
@@ -15,8 +16,7 @@ func init() {
 }
 
 func main() {
-
 	db.Init()
+	config.InitAdmin()
 	beego.Run()
-
 }

+ 23 - 5
server/middleware/auth.go

@@ -8,9 +8,19 @@ import (
 	"github.com/astaxie/beego/session"
 	"github.com/beego/beego/v2/client/httplib"
 	"net/http"
+	"server/config"
 	"server/models"
+	"strings"
 )
 
+// 白名单,不需要登录即可访问
+var whitelist = map[string]bool{
+	"/user/login":      true,
+	"/user/register":   true,
+	"/oauth2":          true,
+	"/oauth2/callback": true,
+}
+
 type ThirdSession struct {
 	Enable     bool
 	CookieName string
@@ -62,6 +72,14 @@ func checkThirdSession(ctx *context.Context, sess session.Store) {
 }
 
 func AuthFilter(ctx *context.Context) {
+	path := ctx.Request.RequestURI
+	path = strings.TrimSuffix(path, "/")
+	path = strings.TrimPrefix(path, config.Config.BaseApi)
+	if whitelist[path] {
+		logs.Debug("in whitelist ,skip ", ctx.Request.RequestURI, path)
+		return
+	}
+	logs.Info(fmt.Sprintf("auth: %s,%s", ctx.Request.RequestURI, path))
 	sess := ctx.Input.CruSession
 	defer sess.SessionRelease(ctx.ResponseWriter)
 	data := sess.Get("user")
@@ -70,20 +88,20 @@ func AuthFilter(ctx *context.Context) {
 	}
 	data = sess.Get("user")
 	if data == nil {
-		writeForbidden(ctx.ResponseWriter)
+		WriteForbidden(ctx.ResponseWriter)
 		return
 	}
 	user := data.(models.User)
 	if len(user.Account) == 0 {
-		writeForbidden(ctx.ResponseWriter)
+		WriteForbidden(ctx.ResponseWriter)
 		return
 	}
 	logs.Info(fmt.Sprintf("request uri: %s, uid: %s", ctx.Request.RequestURI, user.Account))
 }
 
-func writeForbidden(w http.ResponseWriter) {
-	w.WriteHeader(403)
-	_, err := w.Write([]byte("403 Forbidden\n"))
+func WriteForbidden(w http.ResponseWriter) {
+	w.WriteHeader(401)
+	_, err := w.Write([]byte("401 Unauthorized\n"))
 	if err != nil {
 		logs.Warn("writeForbidden write error", err)
 		return

+ 8 - 5
server/routers/router.go

@@ -32,18 +32,21 @@ func init() {
 		// file upload download
 		beego.NSRouter("/file", &controllers.FileController{}),
 		beego.NSRouter("/file/deploy", &controllers.FileController{}, "post:Deploy"),
+
+		beego.NSRouter("/user/login", &controllers.UserController{}, "post:Login"),
+		beego.NSRouter("/user/register", &controllers.UserController{}, "post:Register"),
+		beego.NSRouter("/oauth2", &controllers.UserController{}),
+		beego.NSRouter("/oauth2/callback", &controllers.UserController{}, "post:Callback"),
 	)
 	beego.AddNamespace(ns)
 
 	beego.InsertFilter(fmt.Sprintf("%s/**", config.BaseApi), beego.BeforeRouter, middleware.AuthFilter)
 
-	beego.Router("/nginx-ui/config.js", &controllers.ConfigController{})
+	beego.Router(fmt.Sprintf("%s/config.js", config.ContextPath), &controllers.ConfigController{})
 	// portal static assets
-	beego.SetStaticPath("/nginx-ui", "static/web")
-	beego.SetStaticPath("/web", "static/web")
-
+	beego.SetStaticPath(config.ContextPath, "static/web")
 	beego.Get("/", func(ctx *context.Context) {
-		ctx.Redirect(301, "/nginx-ui/index.html")
+		ctx.Redirect(301, fmt.Sprintf("%s/index.html", config.ContextPath))
 	})
 
 	beego.ErrorHandler("404", func(writer http.ResponseWriter, request *http.Request) {

+ 42 - 12
server/utils/cert.go

@@ -1,12 +1,42 @@
-package utils
-
-import (
-	"crypto/x509"
-	"encoding/pem"
-)
-
-func CheckHttps(pub string) (*x509.Certificate, error) {
-	skBlock, _ := pem.Decode([]byte(pub))
-	cert, err := x509.ParseCertificate(skBlock.Bytes)
-	return cert, err
-}
+package utils
+
+import (
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/pem"
+)
+
+func CheckHttps(pub string) (*x509.Certificate, error) {
+	skBlock, _ := pem.Decode([]byte(pub))
+	cert, err := x509.ParseCertificate(skBlock.Bytes)
+	return cert, err
+}
+
+func GetSHA256HashCode(stringMessage string) string {
+
+	message := []byte(stringMessage) //字符串转化字节数组
+	//创建一个基于SHA256算法的hash.Hash接口的对象
+	hash := sha256.New() //sha-256加密
+	//hash := sha512.New() //SHA-512加密
+	//输入数据
+	hash.Write(message)
+	//计算哈希值
+	bytes := hash.Sum(nil)
+	//将字符串编码为16进制格式,返回字符串
+	hashCode := hex.EncodeToString(bytes)
+	//返回哈希值
+	return hashCode
+
+}
+
+func RandPassword(n int) (string, error) {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	if err != nil {
+		return "", err
+	}
+	return base64.StdEncoding.EncodeToString(b), nil
+}

+ 41 - 41
src/App.css

@@ -1,41 +1,41 @@
-#nginx_ui_root {
-    height: 100%;
-    width: 100%;
-    overflow: hidden;
-}
-
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
-  filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  a:nth-of-type(2) .logo {
-    animation: logo-spin infinite 20s linear;
-  }
-}
-
-.card {
-  padding: 2em;
-}
-
-.read-the-docs {
-  color: #888;
-}
+#nginx_ui_root {
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+}
+
+.logo {
+  height: 6em;
+  padding: 1.5em;
+  will-change: filter;
+  transition: filter 300ms;
+}
+.logo:hover {
+  filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+  filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  a:nth-of-type(2) .logo {
+    animation: logo-spin infinite 20s linear;
+  }
+}
+
+.card {
+  padding: 2em;
+}
+
+.read-the-docs {
+  color: #888;
+}

+ 28 - 28
src/App.tsx

@@ -1,28 +1,28 @@
-import './App.css'
-import {MyRouter} from "./routes";
-import { Provider } from 'react-redux';
-import { persistor, store } from './store';
-import {PersistGate} from "redux-persist/integration/react";
-import {ConfigProvider} from "antd";
-import zhCN from 'antd/lib/locale/zh_CN';
-import 'antd/dist/antd.css'
-import 'planning-tools/dist/umd/planning-tools.min.css'
-
-function App() {
-
-  return (
-    <>
-      <Provider store={store}>
-        <PersistGate loading persistor={persistor}>
-          <ConfigProvider locale={zhCN}>
-            <MyRouter />
-          </ConfigProvider>
-        </PersistGate>
-
-      </Provider>
-
-    </>
-  )
-}
-
-export default App
+import './App.css'
+import {MyRouter} from "./routes";
+import { Provider } from 'react-redux';
+import { persistor, store } from './store';
+import {PersistGate} from "redux-persist/integration/react";
+import {ConfigProvider} from "antd";
+import zhCN from 'antd/lib/locale/zh_CN';
+import 'antd/dist/antd.css'
+import 'planning-tools/dist/umd/planning-tools.min.css'
+
+function App() {
+
+  return (
+    <>
+      <Provider store={store}>
+        <PersistGate loading persistor={persistor}>
+          <ConfigProvider locale={zhCN}>
+            <MyRouter />
+          </ConfigProvider>
+        </PersistGate>
+
+      </Provider>
+
+    </>
+  )
+}
+
+export default App

+ 27 - 27
src/adapter/index.js

@@ -1,27 +1,27 @@
-/**
- * 神奇了,这个东西居然没有了,可能跟打包工具有关系
- */
-if (!window.caches) {
-    window.caches = {
-        delete(cacheName) {
-            return false
-        },
-        has(cacheName) {
-            return false
-        },
-        keys() {
-            return []
-        },
-        /**
-         *
-         * @param request RequestInfo | URL
-         * @param options MultiCacheQueryOptions
-         */
-        match(request, options) {
-
-        },
-        open(cacheName) {
-
-        }
-    }
-}
+/**
+ * 神奇了,这个东西居然没有了,可能跟打包工具有关系
+ */
+if (!window.caches) {
+    window.caches = {
+        delete(cacheName) {
+            return false
+        },
+        has(cacheName) {
+            return false
+        },
+        keys() {
+            return []
+        },
+        /**
+         *
+         * @param request RequestInfo | URL
+         * @param options MultiCacheQueryOptions
+         */
+        match(request, options) {
+
+        },
+        open(cacheName) {
+
+        }
+    }
+}

+ 119 - 119
src/api/nginx.ts

@@ -1,120 +1,120 @@
-import request from "./request.ts";
-import {BaseResp, INginxCerts, IServerHost} from "../models/api.ts";
-import {INginx, INginxServer} from "../models/nginx.ts";
-import {createServer, createServerHost} from "../pages/nginx/utils/nginx.ts";
-
-
-type RefreshHttpData = {
-  id: number
-  httpConf: string
-  httpData: string
-}
-
-
-export const NginxApis= {
-
-  findAll: () => request.get<BaseResp<INginx[]>>('/nginx'),
-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  // @ts-ignore
-  updateOrAdd: (data: Partial<INginx>) => request.post<BaseResp<INginx>>('/nginx', data, { disableErrorMsg: true, timeout: 60000 } as any),
-  /**
-   * 同步配置文件到本地,仅需要传递id  和httpConf, httpData 三个参数
-   * @param nginx
-   */
-  refreshHttp: (nginx: RefreshHttpData) => {
-    return request.post('/http/refresh', nginx, { timeout: 60000 })
-  },
-  getNginx: (id:number) => request.get<BaseResp<{nginx: INginx, servers: IServerHost[]}>>(`/nginx/${id}`),
-  delNginx: (id:number) => request.delete(`/nginx/${id}`),
-  /**
-   * 不更改配置文件,仅保存数据,方便某些特殊情况,一直手动修改配置文件
-   * @param nginx
-   * @param server
-   */
-  // add or update
-  updateServer: (nginx: INginx,server: Partial<INginxServer>) => {
-    const serverHost: Partial<IServerHost> = createServerHost(nginx,server);
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    return request.post<BaseResp<IServerHost>>('/server',serverHost, { disableErrorMsg: true } as any)
-      .then(({data})=>{
-        if (data.data){
-          return createServer(data.data)
-        }
-        return Promise.reject(data)
-      })
-  },
-  /**
-   * 更改配置文件,保存数据
-   * @param nginx
-   * @param server
-   */
-  refreshServer: (server: Partial<IServerHost>) => {
-    return request.post('/server/refresh', server, { timeout: 60000 })
-  },
-  deleteServer: (server: INginxServer) => request.delete(`/server`,{ data: { id: server.id}}),
-  status: (id:number) => request.post(`/nginx/${id}/status`, { }, { timeout: 60000 }),
-  startNginx: (id:number) => request.post(`/nginx/${id}/start`, { }, { timeout: 60000 }),
-  stopNginx: (id:number) => request.post(`/nginx/${id}/stop`, { }, { timeout: 60000 }),
-
-  /**
-   * 获取证书信息,不传name,则返回所有证书文件信息,传了name,则返回该证书的内容
-   * @param id
-   * @param name
-   */
-  getCerts: (id: number,name?: string) => request.get(`/nginx/${id}/certs`, { params: { name }}),
-  /**
-   * 保存证书信息
-   * @param id
-   * @param data
-   */
-  saveCerts: (id: number,data: INginxCerts) => request.post(`/nginx/${id}/certs`, data),
-  delCerts: (nginxId: number,id: number) => request.delete(`/nginx/${nginxId}/certs`, { params: { id } }),
-  /**
-   * 从配置的数据目录中同步
-   * @param id
-   * @param name
-   */
-  syncCerts: (id: number) => request.post(`/nginx/${id}/certs/sync`),
-
-}
-
-
-export type IDeployReq  ={
-  key: string
-  nginxId: number
-  /**
-   * 部署目录,资源部署目录,一般是root+name 或者是alias
-   */
-  dir: string
-  /**
-   * 是否清空文件夹再部署
-   */
-  clear?: boolean
-}
-/**
- * 文件上传
- */
-export const uploadApis = {
-  uploadFile: (entry: FileSystemFileEntry, id: string) => {
-    return new Promise<File>((resolve, reject) => {
-      entry.file(function (f){
-        resolve(f)
-      },function (err){
-        reject(err)
-      })
-    }).then(file=>{
-      const formData = new FormData()
-      formData.append("file", file)
-      formData.append("Path", entry.fullPath)
-      formData.append("Key", id)
-      return request.post('/file',formData, {
-        withCredentials: true,
-        headers: {
-          'Content-type' : 'multipart/form-data'
-        }
-      })
-    })
-  },
-  deploy:(data: IDeployReq)=>request.post('/file/deploy', data, {timeout: 120000}),
+import request from "./request.ts";
+import {BaseResp, INginxCerts, IServerHost} from "../models/api.ts";
+import {INginx, INginxServer} from "../models/nginx.ts";
+import {createServer, createServerHost} from "../pages/nginx/utils/nginx.ts";
+
+
+type RefreshHttpData = {
+  id: number
+  httpConf: string
+  httpData: string
+}
+
+
+export const NginxApis= {
+
+  findAll: () => request.get<BaseResp<INginx[]>>('/nginx'),
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-ignore
+  updateOrAdd: (data: Partial<INginx>) => request.post<BaseResp<INginx>>('/nginx', data, { disableErrorMsg: true, timeout: 60000 } as any),
+  /**
+   * 同步配置文件到本地,仅需要传递id  和httpConf, httpData 三个参数
+   * @param nginx
+   */
+  refreshHttp: (nginx: RefreshHttpData) => {
+    return request.post('/http/refresh', nginx, { timeout: 60000 })
+  },
+  getNginx: (id:number) => request.get<BaseResp<{nginx: INginx, servers: IServerHost[]}>>(`/nginx/${id}`),
+  delNginx: (id:number) => request.delete(`/nginx/${id}`),
+  /**
+   * 不更改配置文件,仅保存数据,方便某些特殊情况,一直手动修改配置文件
+   * @param nginx
+   * @param server
+   */
+  // add or update
+  updateServer: (nginx: INginx,server: Partial<INginxServer>) => {
+    const serverHost: Partial<IServerHost> = createServerHost(nginx,server);
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    return request.post<BaseResp<IServerHost>>('/server',serverHost, { disableErrorMsg: true } as any)
+      .then(({data})=>{
+        if (data.data){
+          return createServer(data.data)
+        }
+        return Promise.reject(data)
+      })
+  },
+  /**
+   * 更改配置文件,保存数据
+   * @param nginx
+   * @param server
+   */
+  refreshServer: (server: Partial<IServerHost>) => {
+    return request.post('/server/refresh', server, { timeout: 60000 })
+  },
+  deleteServer: (server: INginxServer) => request.delete(`/server`,{ data: { id: server.id}}),
+  status: (id:number) => request.post(`/nginx/${id}/status`, { }, { timeout: 60000 }),
+  startNginx: (id:number) => request.post(`/nginx/${id}/start`, { }, { timeout: 60000 }),
+  stopNginx: (id:number) => request.post(`/nginx/${id}/stop`, { }, { timeout: 60000 }),
+
+  /**
+   * 获取证书信息,不传name,则返回所有证书文件信息,传了name,则返回该证书的内容
+   * @param id
+   * @param name
+   */
+  getCerts: (id: number,name?: string) => request.get(`/nginx/${id}/certs`, { params: { name }}),
+  /**
+   * 保存证书信息
+   * @param id
+   * @param data
+   */
+  saveCerts: (id: number,data: INginxCerts) => request.post(`/nginx/${id}/certs`, data),
+  delCerts: (nginxId: number,id: number) => request.delete(`/nginx/${nginxId}/certs`, { params: { id } }),
+  /**
+   * 从配置的数据目录中同步
+   * @param id
+   * @param name
+   */
+  syncCerts: (id: number) => request.post(`/nginx/${id}/certs/sync`),
+
+}
+
+
+export type IDeployReq  ={
+  key: string
+  nginxId: number
+  /**
+   * 部署目录,资源部署目录,一般是root+name 或者是alias
+   */
+  dir: string
+  /**
+   * 是否清空文件夹再部署
+   */
+  clear?: boolean
+}
+/**
+ * 文件上传
+ */
+export const uploadApis = {
+  uploadFile: (entry: FileSystemFileEntry, id: string) => {
+    return new Promise<File>((resolve, reject) => {
+      entry.file(function (f){
+        resolve(f)
+      },function (err){
+        reject(err)
+      })
+    }).then(file=>{
+      const formData = new FormData()
+      formData.append("file", file)
+      formData.append("Path", entry.fullPath)
+      formData.append("Key", id)
+      return request.post('/file',formData, {
+        withCredentials: true,
+        headers: {
+          'Content-type' : 'multipart/form-data'
+        }
+      })
+    })
+  },
+  deploy:(data: IDeployReq)=>request.post('/file/deploy', data, {timeout: 120000}),
 }

+ 68 - 68
src/api/request.ts

@@ -1,68 +1,68 @@
-import axios, {AxiosResponse} from 'axios';
-import {BaseResp} from "../models/api.ts";
-import {Message, Notify} from "planning-tools";
-console.log('env', import.meta.env)
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-const CONFIG = window.CONFIG;
-if (!CONFIG.baseApi){
-  CONFIG.baseApi = '/api'
-}
-
-/**
- * 支持网络请求
- * @type {AxiosInstance}
- */
-// create an axios instance
-const request = axios.create({
-  baseURL: CONFIG.baseApi,
-  withCredentials: true, // send cookies when cross-domain requests
-  timeout: 10000, // request timeout
-});
-
-request.interceptors.request.use(
-  (config) => {
-    if (!config.headers){
-      config.headers = {}
-    }
-    config.headers["Authorization"] = "token"
-    return config;
-  },
-  (error) => {
-    // do something with request error
-    console.log(error); // for debug
-    return Promise.reject(error);
-  },
-);
-
-request.interceptors.response.use((resp: AxiosResponse<BaseResp>)=>{
-  const disableErrorMsg = (resp.config as any)['disableErrorMsg']
-  if (resp.data && resp.data.code == 0){
-    return resp
-  }else if (resp.data) {
-    (!disableErrorMsg) && Notify.warn(resp.data.msg)
-    return Promise.reject(resp.data)
-  }else {
-    (!disableErrorMsg) && Notify.warn("请求错误")
-    return resp
-  }
-},error => {
-  let errData: any = {
-    code: 10
-  }
-  const disableErrorMsg = (error.request?.config as any)?.['disableErrorMsg']
-  if (error.response && error.response.data){
-    errData = error.response.data
-  }else if (error.message){
-    errData.msg = error.message;
-  }
-  if (!errData.code){
-    errData.msg = 'request fail'
-  }
-  (!disableErrorMsg)&& Message.error(errData.msg)
-  return Promise.reject(errData)
-})
-
-
-export default request
+import axios, {AxiosResponse} from 'axios';
+import {BaseResp} from "../models/api.ts";
+import {Message, Notify} from "planning-tools";
+console.log('env', import.meta.env)
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+const CONFIG = window.CONFIG;
+if (!CONFIG.baseApi){
+  CONFIG.baseApi = '/api'
+}
+
+/**
+ * 支持网络请求
+ * @type {AxiosInstance}
+ */
+// create an axios instance
+const request = axios.create({
+  baseURL: CONFIG.baseApi,
+  withCredentials: true, // send cookies when cross-domain requests
+  timeout: 10000, // request timeout
+});
+
+request.interceptors.request.use(
+  (config) => {
+    if (!config.headers){
+      config.headers = {}
+    }
+    config.headers["Authorization"] = "token"
+    return config;
+  },
+  (error) => {
+    // do something with request error
+    console.log(error); // for debug
+    return Promise.reject(error);
+  },
+);
+
+request.interceptors.response.use((resp: AxiosResponse<BaseResp>)=>{
+  const disableErrorMsg = (resp.config as any)['disableErrorMsg']
+  if (resp.data && resp.data.code == 0){
+    return resp
+  }else if (resp.data) {
+    (!disableErrorMsg) && Notify.warn(resp.data.msg)
+    return Promise.reject(resp.data)
+  }else {
+    (!disableErrorMsg) && Notify.warn("请求错误")
+    return resp
+  }
+},error => {
+  let errData: any = {
+    code: 10
+  }
+  const disableErrorMsg = (error.request?.config as any)?.['disableErrorMsg']
+  if (error.response && error.response.data){
+    errData = error.response.data
+  }else if (error.message){
+    errData.msg = error.message;
+  }
+  if (!errData.code){
+    errData.msg = 'request fail'
+  }
+  (!disableErrorMsg)&& Message.error(errData.msg)
+  return Promise.reject(errData)
+})
+
+
+export default request

+ 29 - 29
src/components/BackButton.tsx

@@ -1,29 +1,29 @@
-/**
- * @author tuonian
- * @date 2023/6/30
- */
-import {Button} from "antd";
-import {ArrowLeftOutlined} from "@ant-design/icons";
-import {useNavigate} from "react-router";
-
-
-type IProps = {
-  to?: string
-}
-export const BackButton = ({to}: IProps) => {
-
-  const navigate = useNavigate()
-
-  const goBack = ()=>{
-    if (to){
-      navigate(to, { replace: true })
-    }else {
-      navigate(-1)
-    }
-  }
-
-  return (<Button
-    ghost style={{color: '#1890ff',marginRight: 10}}
-                  onClick={goBack}
-                  icon={<ArrowLeftOutlined />} /> )
-}
+/**
+ * @author tuonian
+ * @date 2023/6/30
+ */
+import {Button} from "antd";
+import {ArrowLeftOutlined} from "@ant-design/icons";
+import {useNavigate} from "react-router";
+
+
+type IProps = {
+  to?: string
+}
+export const BackButton = ({to}: IProps) => {
+
+  const navigate = useNavigate()
+
+  const goBack = ()=>{
+    if (to){
+      navigate(to, { replace: true })
+    }else {
+      navigate(-1)
+    }
+  }
+
+  return (<Button
+    ghost style={{color: '#1890ff',marginRight: 10}}
+                  onClick={goBack}
+                  icon={<ArrowLeftOutlined />} /> )
+}

+ 699 - 699
src/config/nginx_form.json

@@ -1,699 +1,699 @@
-{
-  "server": [
-    {
-      "key": "server_name",
-      "title": "域名",
-      "type": "string",
-      "ruleType": "reg",
-      "pattern": "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
-      "placeholder": "请填写域名",
-      "description": "eg. demo.domain.cn",
-      "width": 300
-    },
-    {
-      "key": "listen",
-      "type": "int",
-      "title": "监听",
-      "min": 0,
-      "max": 65535,
-      "width": 300
-    },
-    {
-      "key": "enable",
-      "title": "启用",
-      "type": "switch",
-      "description": "是否启用,如果不启用,将不会渲染该配置"
-    },
-    {
-      "key": "ssl",
-      "title": "https",
-      "type": "switch",
-      "cascade": {
-        "true": [
-          {
-            "key": "certName",
-            "type": "certs",
-            "placeholder": "选择SSL证书",
-            "title": "SSL证书",
-            "description": "选择SSL证书,如果没有,请填到“SSL证书”管理界面添加证书"
-          }
-        ]
-      }
-    },
-    {
-      "key": "http2",
-      "title": "http2",
-      "type": "switch",
-      "cascade": {
-        "true": [
-          {
-            "key": "http2_max_concurrent_streams",
-            "value": 1024,
-            "title": "最大并发流",
-            "type": "int",
-            "placeholder": "http2_max_concurrent_streams",
-            "description": "http2_max_concurrent_streams",
-            "width": 300
-          }
-        ]
-      }
-    },
-    {
-      "key": "proxy_settings",
-      "title": "更多代理设置",
-      "type": "proxy_settings",
-      "required": false,
-      "description": "更多代理设置"
-    },
-    {
-      "type": "locations",
-      "title": "代理/站点",
-      "key": "locations",
-      "required": false,
-      "description": "静态资源或者反向代理,路由规则"
-    },
-    {
-      "type": "gzip",
-      "title": "压缩配置",
-      "key": "gzip",
-      "required": false,
-      "description": "gzip"
-    },
-    {
-      "type": "auth",
-      "title": "鉴权",
-      "key": "auth_request",
-      "required": false,
-      "description": "ngx_http_auth_request_module:实现了基于一子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的"
-    },
-    {
-      "key": "rewrite",
-      "type": "object",
-      "title": "rewrite",
-      "required": false,
-      "hideHeader": true,
-      "description": "格式:rewrite < regex > < replacement > [flag]",
-      "items": [
-        {
-          "key": "regex",
-          "title": "正则表达式",
-          "type": "string",
-          "width": 180,
-          "placeholder": "<regex> 正则匹配",
-          "required": false
-        },
-        {
-          "key": "replacement",
-          "title": "跳转路径",
-          "type": "string",
-          "placeholder": "<replacement> 跳转后的内容",
-          "width": 300,
-          "required": false
-        },
-        {
-          "key": "flag",
-          "title": "flag",
-          "type": "select",
-          "option": ["last","break","redirect","permanent"],
-          "width": 120,
-          "placeholder": "[flag] 标记",
-          "description": "last: 相当于Apache的【L】标记,表示完成rewrite;\nbreak:本条规则匹配完成即终止,不在匹配后面的任何规则;\nredirect: 返回302临时重定向,浏览器地址栏会显示跳转后的URL地址,爬虫不会更新url;\npermanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址,爬虫更新url;"
-        }
-      ]
-    },
-    {
-      "key": "tmp_custom_config",
-      "title": "自定义配置",
-      "type": "textarea",
-      "hideHeader": true,
-      "description": "自定义配置,注意,每行结尾需要加“;”号,将会拼接在最后,不做任何修改,请注意格式",
-      "required": false,
-      "trim": false,
-      "width": 600
-    },
-    {
-      "type": "divider",
-      "key": "tmp_more_settings",
-      "collapsible": true,
-      "value": false,
-      "title": "更多设置",
-      "items": [
-        {
-          "key": "keepalive_timeout",
-          "type": "string",
-          "required": false,
-          "title": "keepalive_timeout",
-          "description": "eg. 10s",
-          "min": 0,
-          "ruleType": "reg",
-          "pattern": "(\\d)(s|m|h)$"
-        },
-        {
-          "key": "client_max_body_size",
-          "type": "string",
-          "placeholder": "请求体的最大大小",
-          "title": "最大请求体大小",
-          "description": "eg. 500m 20m",
-          "required": false,
-          "value": "10m"
-        },
-        {
-          "key": "access_log",
-          "type": "string",
-          "placeholder": "access日志文件路径",
-          "required": false,
-          "title": "Access日志"
-        },
-        {
-          "key": "charset",
-          "title": "编码",
-          "required": false,
-          "description": "charset"
-        },
-        {
-          "key": "ssl_session_timeout",
-          "type": "string",
-          "placeholder": "ssl_session_timeout",
-          "title": "ssl_session_timeout",
-          "description": "eg. 5m 60s",
-          "required": false
-        },
-        {
-          "key": "ssl_prefer_server_ciphers",
-          "type": "select",
-          "placeholder": "ssl_prefer_server_ciphers",
-          "title": "ssl_prefer_server_ciphers",
-          "option": ["on","off"],
-          "value": "on",
-          "required": false,
-          "width": 260
-        },
-        {
-          "key": "ssl_ciphers",
-          "type": "string",
-          "placeholder": "ssl_ciphers",
-          "title": "ssl_ciphers",
-          "description": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
-          "required": false,
-          "width": 450
-        },
-        {
-          "key": "ssl_protocols",
-          "type": "select",
-          "mode": "multiple",
-          "placeholder": "ssl_protocols",
-          "title": "SSL协议",
-          "option": ["TLSv1","TLSv1.1","TLSv1.2","TLSv2","TLSv3"],
-          "required": false,
-          "width": 450
-        }
-      ]
-    }
-  ],
-  "addNginx": [
-    {
-      "type": "string",
-      "key": "name",
-      "title": "名称"
-    },
-    {
-      "key": "isLocal",
-      "type": "switch",
-      "description": "本地实例,直接在服务器上运行名称,非本地实例,需要配置SSH连接信息,使用SSH执行相关命令",
-      "value": true,
-      "title": "本地实例",
-      "cascade": {
-        "false": [
-          {
-            "type": "string",
-            "key": "ipAddr",
-            "title": "IP地址"
-          },
-          {
-            "type": "int",
-            "key": "port",
-            "title": "端口"
-          },
-          {
-            "type": "string",
-            "key": "user",
-            "title": "用户名"
-          },
-          {
-            "type": "password",
-            "key": "password",
-            "title": "密码"
-          }
-        ]
-      }
-    }
-  ],
-  "nginxSettings": [
-    {
-      "key": "isServer",
-      "title": "服务方式运行",
-      "type": "switch",
-      "description": "以服务方式运行,则使用service nginx start|stop|reload 等命令,否则使用nginx -s reload|stop 等命令"
-    },
-    {
-      "key": "nginxPath",
-      "title": "nginx位置",
-      "type": "string",
-      "description": "nginx的文件所在的绝对路径,默认为:/usr/sbin/nginx,可使用nginx -V 查看参数--sbin-path;或者先填写,保存成功之后返回版本信息,可参考修正即可",
-      "value": "/usr/sbin/nginx"
-    },
-    {
-      "key": "nginxDir",
-      "title": "nginx配置目录",
-      "type": "string",
-      "description": "nginx的配置文件所在的目录,即nginx.conf所在的目录,一般为:/etc/nginx,可使用nginx -V 查看参数 --prefix",
-      "value": "/etc/nginx"
-    },
-    {
-      "key":"dataDir",
-      "type": "string",
-      "title": "数据目录",
-      "description": "nginx的自定义配置文件所在目录"
-    },
-    {
-      "key": "remark",
-      "title": "备注信息",
-      "placeholder": "输入备注",
-      "type": "textarea",
-      "required": false,
-      "trim": false
-    }
-  ],
-  "nginxConf": [
-    {
-      "key": "user",
-      "value": "nginx",
-      "title": "user",
-      "placeholder": "nginx user"
-    },
-    {
-      "key": "worker_processes",
-      "title": "工作进程数量",
-      "type": "string",
-      "ruleType": "reg",
-      "pattern": "^(auto|\\d+)$",
-      "description": "auto或者指定数量"
-    },
-    {
-      "key": "error_log",
-      "title": "错误日志路径",
-      "type": "string",
-      "value": "/var/log/nginx/error.log notice",
-      "description": "eg. /var/log/nginx/error.log notice"
-    },
-    {
-      "key": "pid",
-      "title": "pid位置",
-      "type": "string",
-      "value": "/var/run/nginx.pid",
-      "description": "eg. /var/run/nginx.pid"
-    },
-    {
-      "key": "temp.events",
-      "type": "divider",
-      "title": "events配置",
-      "description": "nginx events 模块主要是nginx 和用户交互网络连接优化的配置内容",
-      "items": [
-        {
-          "key": "events.accept_mutex",
-          "title": "accept_mutex",
-          "type": "switch",
-          "value": true,
-          "required": false,
-          "description": "这个配置主要可以用来解决常说的\"惊群\"问题。大致意思是在某一个时刻,客户端发来一个请求连接,Nginx后台是以多进程的工作模式,也就是说有多个worker进程会被同时唤醒,但是最终只会有一个进程可以获取到连接,如果每次唤醒的进程数目太多,就会影响Nginx的整体性能。如果将上述值设置为on(开启状态),将会对多个Nginx进程接收连接进行序列号,一个个来唤醒接收,就防止了多个进程对连接的争抢"
-        },
-        {
-          "key": "events.worker_connections",
-          "type": "int",
-          "layout": "form",
-          "title": "最大连接数",
-          "description": "用来配置单个worker进程最大的连接数,nginx 默认连接数是1024",
-          "min": 0,
-          "max": 65536,
-          "value": 1024,
-          "required": false
-        },
-        {
-          "key": "events.multi_accept",
-          "title": "multi_accept",
-          "description": "用来设置是否允许同时接收多个网络连接",
-          "type": "switch",
-          "value": false,
-          "required": false
-        },
-        {
-          "key": "events.use",
-          "title": "网络驱动",
-          "description": "用来设置Nginx服务器选择哪种事件驱动来处理网络消息;另外这些值的选择,我们也可以在编译的时候使用:–with-select_module、–without-select_module、 --with-poll_module、–without-poll_module来设置是否需要将对应的事件驱动模块编译到Nginx的内核",
-          "type": "select",
-          "option": ["select","poll","epoll","kqueue"],
-          "required": false
-        }
-      ]
-    },
-    {
-      "key": "temp.http",
-      "title": "http配置",
-      "type": "divider",
-      "items": [
-        {
-          "key": "http.include",
-          "type": "string",
-          "value": "/etc/nginx/mime.types",
-          "title": "include"
-        },
-        {
-          "key": "http.default_type",
-          "type": "string",
-          "value": "application/octet-stream",
-          "title": "default_type"
-        },
-        {
-          "key": "http.log_format",
-          "title": "日志格式",
-          "type": "array",
-          "items": [
-            {
-              "type": "textarea",
-              "key": "name",
-              "value": "main",
-              "title": "格式名称",
-              "rows": 4,
-              "placeholder": "日志格式名称,eg. main compression",
-              "width": 200,
-              "description": "日志格式名称,eg. main compression log1 log2",
-              "trim": false
-            },
-            {
-              "type": "textarea",
-              "key": "content",
-              "value": "",
-              "title": "日志格式",
-              "rows": 4,
-              "width": 400,
-              "trim": false,
-              "placeholder": "'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'",
-              "description": "参数                      说明                                         示例\n$remote_addr             客户端地址                                    211.28.65.253\n$remote_user             客户端用户名称                                --\n$time_local              访问时间和时区                                18/Jul/2012:17:00:01 +0800\n$request                 请求的URI和HTTP协议                           \"GET /article-10000.html HTTP/1.1\"\n$http_host               请求地址,即浏览器中你输入的地址(IP或域名)     www.wang.com 192.168.100.100\n$status                  HTTP请求状态                                  200\n$upstream_status         upstream状态                                  200\n$body_bytes_sent         发送给客户端文件内容大小                        1547\n$http_referer            url跳转来源                                   https://www.baidu.com/\n$http_user_agent         用户终端浏览器等信息                           \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; GTB7.0; .NET4.0C;\n$ssl_protocol            SSL协议版本                                   TLSv1\n$ssl_cipher              交换数据中的算法                               RC4-SHA\n$upstream_addr           后台upstream的地址,即真正提供服务的主机地址     10.10.10.100:80\n$request_time            整个请求的总时间                               0.205\n$upstream_response_time  请求过程中,upstream响应时间                    0.002\n"
-            }
-          ]
-        },
-        {
-          "key": "http.access_log",
-          "title": "访问日志",
-          "type": "object",
-          "value": "/var/log/nginx/access.log  main",
-          "items": [
-            {
-              "key": "name",
-              "type": "string",
-              "title": "格式标签",
-              "placeholder": "日志格式配置的格式名称",
-              "description": "输入日志格式配置的格式名称",
-              "width": 200
-            },
-            {
-              "key": "path",
-              "type": "string",
-              "title": "日志路径",
-              "width": 250
-            }
-          ]
-        },
-        {
-          "key": "http.sendfile",
-          "type": "select",
-          "required": false,
-          "option": ["on","off"],
-          "title": "sendfile"
-        },
-        {
-          "key": "http.tcp_nopush",
-          "type": "select",
-          "required": false,
-          "option": ["on","off"],
-          "title": "tcp_nopush"
-        },
-        {
-          "type": "gzip",
-          "title": "压缩配置",
-          "key": "http.gzip",
-          "required": false,
-          "description": "gzip"
-        },
-        {
-          "key": "http.keepalive_timeout",
-          "type": "int",
-          "required": false,
-          "title": "keepalive_timeout",
-          "description": "单位为秒(s), 0表示不限制",
-          "min": 0
-        },
-        {
-          "key": "http.proxy_settings",
-          "title": "代理设置",
-          "type": "proxy_settings",
-          "required": false
-        },
-        {
-          "key": "http.more",
-          "type": "textarea",
-          "required": false,
-          "title": "更多配置",
-          "description": "自定义配置,每行需要有分隔符号",
-          "trim": false
-        }
-      ]
-    },
-    {
-      "key": "stream",
-      "type": "divider",
-      "collapsible": true,
-      "title": "TCP/UDP配置",
-      "description": "stream配置,需要注意安装的nginx版本是否支持;默认情况下,没有构建此模块。 -必须使用-with stream配置参数启用",
-      "items": [
-        {
-          "key": "stream.log_format",
-          "title": "日志格式",
-          "type": "array",
-          "items": [
-            {
-              "type": "textarea",
-              "key": "name",
-              "value": "tcp_format",
-              "title": "格式名称",
-              "rows": 4,
-              "placeholder": "日志格式名称,eg. tcp_format",
-              "width": 200,
-              "description": "日志格式名称,eg. tcp_format",
-              "trim": false
-            },
-            {
-              "type": "textarea",
-              "key": "content",
-              "value": "",
-              "title": "日志格式",
-              "rows": 4,
-              "width": 400,
-              "trim": false,
-              "placeholder": "'$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'"
-            }
-          ]
-        },
-        {
-          "key": "stream.access_log",
-          "title": "访问日志",
-          "type": "object",
-          "value": {
-            "name": "tcp_format",
-            "path": "/var/log/nginx/access_stream.log"
-          },
-          "items": [
-            {
-              "key": "name",
-              "type": "string",
-              "title": "格式标签",
-              "placeholder": "日志格式配置的格式名称",
-              "description": "输入日志格式配置的格式名称",
-              "width": 200,
-              "value": "tcp_format"
-            },
-            {
-              "key": "path",
-              "type": "string",
-              "title": "日志路径",
-              "width": 250
-            }
-          ]
-        },
-        {
-          "key": "stream.error_log",
-          "title": "错误日志",
-          "type": "string",
-          "value": "/var/log/nginx/error_stream.log"
-        }
-      ]
-    }
-  ],
-  "upstream": [
-    {
-      "title": "名称",
-      "key": "name",
-      "width": 100,
-      "description": "名称相同则为同一组负载均衡,只支持英文字母",
-      "type": "string"
-    },
-    {
-      "title": "负载方式",
-      "key": "type",
-      "type": "select",
-      "option": ["ip_hash","weight"],
-      "width": 100,
-      "required": false,
-      "description": "ip_hash: 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session不能跨服务器的问题。如果后端服务器down掉,要手工down掉;weight:指定轮询几率,weight和访问比率成正比,如果后端服务器down掉,能自动剔除。"
-    },
-    {
-      "title": "是否启用",
-      "key": "enable",
-      "type": "switch",
-      "value": true
-    },
-    {
-      "title": "服务配置",
-      "type": "array",
-      "key": "servers",
-      "items": [
-        {
-          "title": "主机",
-          "type": "string",
-          "key": "host",
-          "description": "后端服务IP",
-          "width": 150
-        },
-        {
-          "title": "端口",
-          "type": "int",
-          "key": "port",
-          "description": "后端服务端口",
-          "width": 100
-        },
-        {
-          "title": "权重",
-          "type": "int",
-          "key": "weight",
-          "description": "权重,ip_hash模式下不生效,数字越大,越高,为0将剔除",
-          "width": 80,
-          "min": 0,
-          "value": 100
-        },
-        {
-          "title": "状态/角色",
-          "type": "select",
-          "key": "status",
-          "option": ["normal","down","backup"],
-          "description": "weight,backup 不能和 ip_hash 关键字一起使用;down:表示当前的server暂时不参与负载",
-          "width": 100,
-          "required": false
-        },
-        {
-          "title": "max_fails",
-          "type": "int",
-          "key": "max_fails",
-          "required": false,
-          "description": "最大失败次数,也就是最多进行 3 次尝试,默认为1",
-          "width": 100
-        },
-        {
-          "title": "超时时间",
-          "type": "int",
-          "key": "fail_timeout",
-          "required": false,
-          "description": "超时时间,单位秒,默认值是10s",
-          "width": 100
-        }
-      ]
-    }
-  ],
-  "stream": [
-    {
-      "key": "listen",
-      "type": "int",
-      "value": 3306,
-      "title": "端口",
-      "width": 100
-    },
-    {
-      "key": "proxy_pass",
-      "type": "stream_proxy_pass",
-      "title": "后端服务",
-      "description": "IP:PORT 或者upstream的名称",
-      "width": 200
-    },
-    {
-      "key": "enable",
-      "type": "switch",
-      "title": "启用",
-      "description": "如果不启用,不会渲染该组配置信息",
-      "required": false
-    },
-    {
-      "key": "proxy_connect_timeout",
-      "type": "int",
-      "value": 10,
-      "title": "connect_timeout",
-      "placeholder": "与被代理服务器建立连接的超时时间,单位为s",
-      "required": false
-    },
-    {
-      "key": "proxy_timeout",
-      "type": "int",
-      "value": 10,
-      "title": "超时时间",
-      "placeholder": "获取被代理服务器的响应最大超时时间,单位为s",
-      "required": false
-    },
-    {
-      "key": "proxy_next_upstream",
-      "type": "switch",
-      "value": true,
-      "title": "next_upstream",
-      "description": "当被代理的服务器返回错误或超时时,将未返回响应的客户端连接请求传递给upstream中的下一个服务器",
-      "required": false
-    },
-    {
-      "key": "proxy_next_upstream_tries",
-      "type": "int",
-      "value": 3,
-      "title": "最大错误次数",
-      "description": "转发尝试请求最多3次",
-      "required": false
-    },
-    {
-      "key": "proxy_next_upstream_timeout",
-      "type": "int",
-      "value": 10,
-      "title": "总尝试超时时间",
-      "description": "总尝试超时时间,单位为s",
-      "required": false
-    },
-    {
-      "key": "proxy_socket_keepalive",
-      "type": "switch",
-      "value": true,
-      "title": "心跳",
-      "description": "开启SO_KEEPALIVE选项进行心跳检测",
-      "required": false
-    },
-    {
-      "key": "remark",
-      "type": "string",
-      "placeholder": "备注信息",
-      "required": false,
-      "title": "备注"
-    }
-  ]
-}
+{
+  "server": [
+    {
+      "key": "server_name",
+      "title": "域名",
+      "type": "string",
+      "ruleType": "reg",
+      "pattern": "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
+      "placeholder": "请填写域名",
+      "description": "eg. demo.domain.cn",
+      "width": 300
+    },
+    {
+      "key": "listen",
+      "type": "int",
+      "title": "监听",
+      "min": 0,
+      "max": 65535,
+      "width": 300
+    },
+    {
+      "key": "enable",
+      "title": "启用",
+      "type": "switch",
+      "description": "是否启用,如果不启用,将不会渲染该配置"
+    },
+    {
+      "key": "ssl",
+      "title": "https",
+      "type": "switch",
+      "cascade": {
+        "true": [
+          {
+            "key": "certName",
+            "type": "certs",
+            "placeholder": "选择SSL证书",
+            "title": "SSL证书",
+            "description": "选择SSL证书,如果没有,请填到“SSL证书”管理界面添加证书"
+          }
+        ]
+      }
+    },
+    {
+      "key": "http2",
+      "title": "http2",
+      "type": "switch",
+      "cascade": {
+        "true": [
+          {
+            "key": "http2_max_concurrent_streams",
+            "value": 1024,
+            "title": "最大并发流",
+            "type": "int",
+            "placeholder": "http2_max_concurrent_streams",
+            "description": "http2_max_concurrent_streams",
+            "width": 300
+          }
+        ]
+      }
+    },
+    {
+      "key": "proxy_settings",
+      "title": "更多代理设置",
+      "type": "proxy_settings",
+      "required": false,
+      "description": "更多代理设置"
+    },
+    {
+      "type": "locations",
+      "title": "代理/站点",
+      "key": "locations",
+      "required": false,
+      "description": "静态资源或者反向代理,路由规则"
+    },
+    {
+      "type": "gzip",
+      "title": "压缩配置",
+      "key": "gzip",
+      "required": false,
+      "description": "gzip"
+    },
+    {
+      "type": "auth",
+      "title": "鉴权",
+      "key": "auth_request",
+      "required": false,
+      "description": "ngx_http_auth_request_module:实现了基于一子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的"
+    },
+    {
+      "key": "rewrite",
+      "type": "object",
+      "title": "rewrite",
+      "required": false,
+      "hideHeader": true,
+      "description": "格式:rewrite < regex > < replacement > [flag]",
+      "items": [
+        {
+          "key": "regex",
+          "title": "正则表达式",
+          "type": "string",
+          "width": 180,
+          "placeholder": "<regex> 正则匹配",
+          "required": false
+        },
+        {
+          "key": "replacement",
+          "title": "跳转路径",
+          "type": "string",
+          "placeholder": "<replacement> 跳转后的内容",
+          "width": 300,
+          "required": false
+        },
+        {
+          "key": "flag",
+          "title": "flag",
+          "type": "select",
+          "option": ["last","break","redirect","permanent"],
+          "width": 120,
+          "placeholder": "[flag] 标记",
+          "description": "last: 相当于Apache的【L】标记,表示完成rewrite;\nbreak:本条规则匹配完成即终止,不在匹配后面的任何规则;\nredirect: 返回302临时重定向,浏览器地址栏会显示跳转后的URL地址,爬虫不会更新url;\npermanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址,爬虫更新url;"
+        }
+      ]
+    },
+    {
+      "key": "tmp_custom_config",
+      "title": "自定义配置",
+      "type": "textarea",
+      "hideHeader": true,
+      "description": "自定义配置,注意,每行结尾需要加“;”号,将会拼接在最后,不做任何修改,请注意格式",
+      "required": false,
+      "trim": false,
+      "width": 600
+    },
+    {
+      "type": "divider",
+      "key": "tmp_more_settings",
+      "collapsible": true,
+      "value": false,
+      "title": "更多设置",
+      "items": [
+        {
+          "key": "keepalive_timeout",
+          "type": "string",
+          "required": false,
+          "title": "keepalive_timeout",
+          "description": "eg. 10s",
+          "min": 0,
+          "ruleType": "reg",
+          "pattern": "(\\d)(s|m|h)$"
+        },
+        {
+          "key": "client_max_body_size",
+          "type": "string",
+          "placeholder": "请求体的最大大小",
+          "title": "最大请求体大小",
+          "description": "eg. 500m 20m",
+          "required": false,
+          "value": "10m"
+        },
+        {
+          "key": "access_log",
+          "type": "string",
+          "placeholder": "access日志文件路径",
+          "required": false,
+          "title": "Access日志"
+        },
+        {
+          "key": "charset",
+          "title": "编码",
+          "required": false,
+          "description": "charset"
+        },
+        {
+          "key": "ssl_session_timeout",
+          "type": "string",
+          "placeholder": "ssl_session_timeout",
+          "title": "ssl_session_timeout",
+          "description": "eg. 5m 60s",
+          "required": false
+        },
+        {
+          "key": "ssl_prefer_server_ciphers",
+          "type": "select",
+          "placeholder": "ssl_prefer_server_ciphers",
+          "title": "ssl_prefer_server_ciphers",
+          "option": ["on","off"],
+          "value": "on",
+          "required": false,
+          "width": 260
+        },
+        {
+          "key": "ssl_ciphers",
+          "type": "string",
+          "placeholder": "ssl_ciphers",
+          "title": "ssl_ciphers",
+          "description": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
+          "required": false,
+          "width": 450
+        },
+        {
+          "key": "ssl_protocols",
+          "type": "select",
+          "mode": "multiple",
+          "placeholder": "ssl_protocols",
+          "title": "SSL协议",
+          "option": ["TLSv1","TLSv1.1","TLSv1.2","TLSv2","TLSv3"],
+          "required": false,
+          "width": 450
+        }
+      ]
+    }
+  ],
+  "addNginx": [
+    {
+      "type": "string",
+      "key": "name",
+      "title": "名称"
+    },
+    {
+      "key": "isLocal",
+      "type": "switch",
+      "description": "本地实例,直接在服务器上运行名称,非本地实例,需要配置SSH连接信息,使用SSH执行相关命令",
+      "value": true,
+      "title": "本地实例",
+      "cascade": {
+        "false": [
+          {
+            "type": "string",
+            "key": "ipAddr",
+            "title": "IP地址"
+          },
+          {
+            "type": "int",
+            "key": "port",
+            "title": "端口"
+          },
+          {
+            "type": "string",
+            "key": "user",
+            "title": "用户名"
+          },
+          {
+            "type": "password",
+            "key": "password",
+            "title": "密码"
+          }
+        ]
+      }
+    }
+  ],
+  "nginxSettings": [
+    {
+      "key": "isServer",
+      "title": "服务方式运行",
+      "type": "switch",
+      "description": "以服务方式运行,则使用service nginx start|stop|reload 等命令,否则使用nginx -s reload|stop 等命令"
+    },
+    {
+      "key": "nginxPath",
+      "title": "nginx位置",
+      "type": "string",
+      "description": "nginx的文件所在的绝对路径,默认为:/usr/sbin/nginx,可使用nginx -V 查看参数--sbin-path;或者先填写,保存成功之后返回版本信息,可参考修正即可",
+      "value": "/usr/sbin/nginx"
+    },
+    {
+      "key": "nginxDir",
+      "title": "nginx配置目录",
+      "type": "string",
+      "description": "nginx的配置文件所在的目录,即nginx.conf所在的目录,一般为:/etc/nginx,可使用nginx -V 查看参数 --prefix",
+      "value": "/etc/nginx"
+    },
+    {
+      "key":"dataDir",
+      "type": "string",
+      "title": "数据目录",
+      "description": "nginx的自定义配置文件所在目录"
+    },
+    {
+      "key": "remark",
+      "title": "备注信息",
+      "placeholder": "输入备注",
+      "type": "textarea",
+      "required": false,
+      "trim": false
+    }
+  ],
+  "nginxConf": [
+    {
+      "key": "user",
+      "value": "nginx",
+      "title": "user",
+      "placeholder": "nginx user"
+    },
+    {
+      "key": "worker_processes",
+      "title": "工作进程数量",
+      "type": "string",
+      "ruleType": "reg",
+      "pattern": "^(auto|\\d+)$",
+      "description": "auto或者指定数量"
+    },
+    {
+      "key": "error_log",
+      "title": "错误日志路径",
+      "type": "string",
+      "value": "/var/log/nginx/error.log notice",
+      "description": "eg. /var/log/nginx/error.log notice"
+    },
+    {
+      "key": "pid",
+      "title": "pid位置",
+      "type": "string",
+      "value": "/var/run/nginx.pid",
+      "description": "eg. /var/run/nginx.pid"
+    },
+    {
+      "key": "temp.events",
+      "type": "divider",
+      "title": "events配置",
+      "description": "nginx events 模块主要是nginx 和用户交互网络连接优化的配置内容",
+      "items": [
+        {
+          "key": "events.accept_mutex",
+          "title": "accept_mutex",
+          "type": "switch",
+          "value": true,
+          "required": false,
+          "description": "这个配置主要可以用来解决常说的\"惊群\"问题。大致意思是在某一个时刻,客户端发来一个请求连接,Nginx后台是以多进程的工作模式,也就是说有多个worker进程会被同时唤醒,但是最终只会有一个进程可以获取到连接,如果每次唤醒的进程数目太多,就会影响Nginx的整体性能。如果将上述值设置为on(开启状态),将会对多个Nginx进程接收连接进行序列号,一个个来唤醒接收,就防止了多个进程对连接的争抢"
+        },
+        {
+          "key": "events.worker_connections",
+          "type": "int",
+          "layout": "form",
+          "title": "最大连接数",
+          "description": "用来配置单个worker进程最大的连接数,nginx 默认连接数是1024",
+          "min": 0,
+          "max": 65536,
+          "value": 1024,
+          "required": false
+        },
+        {
+          "key": "events.multi_accept",
+          "title": "multi_accept",
+          "description": "用来设置是否允许同时接收多个网络连接",
+          "type": "switch",
+          "value": false,
+          "required": false
+        },
+        {
+          "key": "events.use",
+          "title": "网络驱动",
+          "description": "用来设置Nginx服务器选择哪种事件驱动来处理网络消息;另外这些值的选择,我们也可以在编译的时候使用:–with-select_module、–without-select_module、 --with-poll_module、–without-poll_module来设置是否需要将对应的事件驱动模块编译到Nginx的内核",
+          "type": "select",
+          "option": ["select","poll","epoll","kqueue"],
+          "required": false
+        }
+      ]
+    },
+    {
+      "key": "temp.http",
+      "title": "http配置",
+      "type": "divider",
+      "items": [
+        {
+          "key": "http.include",
+          "type": "string",
+          "value": "/etc/nginx/mime.types",
+          "title": "include"
+        },
+        {
+          "key": "http.default_type",
+          "type": "string",
+          "value": "application/octet-stream",
+          "title": "default_type"
+        },
+        {
+          "key": "http.log_format",
+          "title": "日志格式",
+          "type": "array",
+          "items": [
+            {
+              "type": "textarea",
+              "key": "name",
+              "value": "main",
+              "title": "格式名称",
+              "rows": 4,
+              "placeholder": "日志格式名称,eg. main compression",
+              "width": 200,
+              "description": "日志格式名称,eg. main compression log1 log2",
+              "trim": false
+            },
+            {
+              "type": "textarea",
+              "key": "content",
+              "value": "",
+              "title": "日志格式",
+              "rows": 4,
+              "width": 400,
+              "trim": false,
+              "placeholder": "'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'",
+              "description": "参数                      说明                                         示例\n$remote_addr             客户端地址                                    211.28.65.253\n$remote_user             客户端用户名称                                --\n$time_local              访问时间和时区                                18/Jul/2012:17:00:01 +0800\n$request                 请求的URI和HTTP协议                           \"GET /article-10000.html HTTP/1.1\"\n$http_host               请求地址,即浏览器中你输入的地址(IP或域名)     www.wang.com 192.168.100.100\n$status                  HTTP请求状态                                  200\n$upstream_status         upstream状态                                  200\n$body_bytes_sent         发送给客户端文件内容大小                        1547\n$http_referer            url跳转来源                                   https://www.baidu.com/\n$http_user_agent         用户终端浏览器等信息                           \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; GTB7.0; .NET4.0C;\n$ssl_protocol            SSL协议版本                                   TLSv1\n$ssl_cipher              交换数据中的算法                               RC4-SHA\n$upstream_addr           后台upstream的地址,即真正提供服务的主机地址     10.10.10.100:80\n$request_time            整个请求的总时间                               0.205\n$upstream_response_time  请求过程中,upstream响应时间                    0.002\n"
+            }
+          ]
+        },
+        {
+          "key": "http.access_log",
+          "title": "访问日志",
+          "type": "object",
+          "value": "/var/log/nginx/access.log  main",
+          "items": [
+            {
+              "key": "name",
+              "type": "string",
+              "title": "格式标签",
+              "placeholder": "日志格式配置的格式名称",
+              "description": "输入日志格式配置的格式名称",
+              "width": 200
+            },
+            {
+              "key": "path",
+              "type": "string",
+              "title": "日志路径",
+              "width": 250
+            }
+          ]
+        },
+        {
+          "key": "http.sendfile",
+          "type": "select",
+          "required": false,
+          "option": ["on","off"],
+          "title": "sendfile"
+        },
+        {
+          "key": "http.tcp_nopush",
+          "type": "select",
+          "required": false,
+          "option": ["on","off"],
+          "title": "tcp_nopush"
+        },
+        {
+          "type": "gzip",
+          "title": "压缩配置",
+          "key": "http.gzip",
+          "required": false,
+          "description": "gzip"
+        },
+        {
+          "key": "http.keepalive_timeout",
+          "type": "int",
+          "required": false,
+          "title": "keepalive_timeout",
+          "description": "单位为秒(s), 0表示不限制",
+          "min": 0
+        },
+        {
+          "key": "http.proxy_settings",
+          "title": "代理设置",
+          "type": "proxy_settings",
+          "required": false
+        },
+        {
+          "key": "http.more",
+          "type": "textarea",
+          "required": false,
+          "title": "更多配置",
+          "description": "自定义配置,每行需要有分隔符号",
+          "trim": false
+        }
+      ]
+    },
+    {
+      "key": "stream",
+      "type": "divider",
+      "collapsible": true,
+      "title": "TCP/UDP配置",
+      "description": "stream配置,需要注意安装的nginx版本是否支持;默认情况下,没有构建此模块。 -必须使用-with stream配置参数启用",
+      "items": [
+        {
+          "key": "stream.log_format",
+          "title": "日志格式",
+          "type": "array",
+          "items": [
+            {
+              "type": "textarea",
+              "key": "name",
+              "value": "tcp_format",
+              "title": "格式名称",
+              "rows": 4,
+              "placeholder": "日志格式名称,eg. tcp_format",
+              "width": 200,
+              "description": "日志格式名称,eg. tcp_format",
+              "trim": false
+            },
+            {
+              "type": "textarea",
+              "key": "content",
+              "value": "",
+              "title": "日志格式",
+              "rows": 4,
+              "width": 400,
+              "trim": false,
+              "placeholder": "'$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'"
+            }
+          ]
+        },
+        {
+          "key": "stream.access_log",
+          "title": "访问日志",
+          "type": "object",
+          "value": {
+            "name": "tcp_format",
+            "path": "/var/log/nginx/access_stream.log"
+          },
+          "items": [
+            {
+              "key": "name",
+              "type": "string",
+              "title": "格式标签",
+              "placeholder": "日志格式配置的格式名称",
+              "description": "输入日志格式配置的格式名称",
+              "width": 200,
+              "value": "tcp_format"
+            },
+            {
+              "key": "path",
+              "type": "string",
+              "title": "日志路径",
+              "width": 250
+            }
+          ]
+        },
+        {
+          "key": "stream.error_log",
+          "title": "错误日志",
+          "type": "string",
+          "value": "/var/log/nginx/error_stream.log"
+        }
+      ]
+    }
+  ],
+  "upstream": [
+    {
+      "title": "名称",
+      "key": "name",
+      "width": 100,
+      "description": "名称相同则为同一组负载均衡,只支持英文字母",
+      "type": "string"
+    },
+    {
+      "title": "负载方式",
+      "key": "type",
+      "type": "select",
+      "option": ["ip_hash","weight"],
+      "width": 100,
+      "required": false,
+      "description": "ip_hash: 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session不能跨服务器的问题。如果后端服务器down掉,要手工down掉;weight:指定轮询几率,weight和访问比率成正比,如果后端服务器down掉,能自动剔除。"
+    },
+    {
+      "title": "是否启用",
+      "key": "enable",
+      "type": "switch",
+      "value": true
+    },
+    {
+      "title": "服务配置",
+      "type": "array",
+      "key": "servers",
+      "items": [
+        {
+          "title": "主机",
+          "type": "string",
+          "key": "host",
+          "description": "后端服务IP",
+          "width": 150
+        },
+        {
+          "title": "端口",
+          "type": "int",
+          "key": "port",
+          "description": "后端服务端口",
+          "width": 100
+        },
+        {
+          "title": "权重",
+          "type": "int",
+          "key": "weight",
+          "description": "权重,ip_hash模式下不生效,数字越大,越高,为0将剔除",
+          "width": 80,
+          "min": 0,
+          "value": 100
+        },
+        {
+          "title": "状态/角色",
+          "type": "select",
+          "key": "status",
+          "option": ["normal","down","backup"],
+          "description": "weight,backup 不能和 ip_hash 关键字一起使用;down:表示当前的server暂时不参与负载",
+          "width": 100,
+          "required": false
+        },
+        {
+          "title": "max_fails",
+          "type": "int",
+          "key": "max_fails",
+          "required": false,
+          "description": "最大失败次数,也就是最多进行 3 次尝试,默认为1",
+          "width": 100
+        },
+        {
+          "title": "超时时间",
+          "type": "int",
+          "key": "fail_timeout",
+          "required": false,
+          "description": "超时时间,单位秒,默认值是10s",
+          "width": 100
+        }
+      ]
+    }
+  ],
+  "stream": [
+    {
+      "key": "listen",
+      "type": "int",
+      "value": 3306,
+      "title": "端口",
+      "width": 100
+    },
+    {
+      "key": "proxy_pass",
+      "type": "stream_proxy_pass",
+      "title": "后端服务",
+      "description": "IP:PORT 或者upstream的名称",
+      "width": 200
+    },
+    {
+      "key": "enable",
+      "type": "switch",
+      "title": "启用",
+      "description": "如果不启用,不会渲染该组配置信息",
+      "required": false
+    },
+    {
+      "key": "proxy_connect_timeout",
+      "type": "int",
+      "value": 10,
+      "title": "connect_timeout",
+      "placeholder": "与被代理服务器建立连接的超时时间,单位为s",
+      "required": false
+    },
+    {
+      "key": "proxy_timeout",
+      "type": "int",
+      "value": 10,
+      "title": "超时时间",
+      "placeholder": "获取被代理服务器的响应最大超时时间,单位为s",
+      "required": false
+    },
+    {
+      "key": "proxy_next_upstream",
+      "type": "switch",
+      "value": true,
+      "title": "next_upstream",
+      "description": "当被代理的服务器返回错误或超时时,将未返回响应的客户端连接请求传递给upstream中的下一个服务器",
+      "required": false
+    },
+    {
+      "key": "proxy_next_upstream_tries",
+      "type": "int",
+      "value": 3,
+      "title": "最大错误次数",
+      "description": "转发尝试请求最多3次",
+      "required": false
+    },
+    {
+      "key": "proxy_next_upstream_timeout",
+      "type": "int",
+      "value": 10,
+      "title": "总尝试超时时间",
+      "description": "总尝试超时时间,单位为s",
+      "required": false
+    },
+    {
+      "key": "proxy_socket_keepalive",
+      "type": "switch",
+      "value": true,
+      "title": "心跳",
+      "description": "开启SO_KEEPALIVE选项进行心跳检测",
+      "required": false
+    },
+    {
+      "key": "remark",
+      "type": "string",
+      "placeholder": "备注信息",
+      "required": false,
+      "title": "备注"
+    }
+  ]
+}

+ 68 - 68
src/config/nginx_template.json

@@ -1,68 +1,68 @@
-{
-  "nginxConf": {
-    "user": "nginx",
-    "worker_processes": "auto",
-    "error_log": "/var/log/nginx/error.log notice",
-    "pid": "/var/run/nginx.pid",
-    "events.worker_connections": 1024,
-    "http.include": "/etc/nginx/mime.types",
-    "http.default_type": "application/octet-stream",
-    "http.log_format": [
-      {
-        "key": "http.log_formatXK1BCq0XKQCMEuV",
-        "name": "main",
-        "content": "'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'"
-      }
-    ],
-    "http.access_log": {
-      "key": "http.access_logqwY8npz3ypNjgIA",
-      "name": "main",
-      "path": "/var/log/nginx/access.log"
-    },
-    "http.sendfile": "off",
-    "http.tcp_nopush": "off",
-    "http.keepalive_timeout": 0,
-    "http.gzip": "off",
-    "stream": true,
-    "stream.log_format": [
-      {
-        "key": "stream.log_format8avSYRKrTjzgzcF",
-        "name": "tcp_format",
-        "content": "'$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'"
-      }
-    ],
-    "stream.access_log": {
-      "key": "stream.access_logpuBlWTJXD64QiOL",
-      "name": "tcp_format",
-      "path": "/var/log/nginx/access_stream.log"
-    },
-    "stream.error_log": "/var/log/nginx/error_stream.log"
-  },
-  "server": {
-    "port": 80,
-    "enable": true,
-    "ssl_session_timeout": "5m",
-    "ssl_ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
-    "ssl_protocols": ["TLSv1","TLSv1.1","TLSv1.2"],
-    "ssl_prefer_server_ciphers": "on",
-    "locations": [
-      {
-        "id": "default",
-        "name": "默认",
-        "match": {
-          "path": "/"
-        },
-        "root": "/data/www",
-        "proxy_type": "static",
-        "enable": true
-      }
-    ]
-  },
-  "location": {
-
-  },
-  "addNginx": {
-
-  },
-  "nginxSettings": {}
-}
+{
+  "nginxConf": {
+    "user": "nginx",
+    "worker_processes": "auto",
+    "error_log": "/var/log/nginx/error.log notice",
+    "pid": "/var/run/nginx.pid",
+    "events.worker_connections": 1024,
+    "http.include": "/etc/nginx/mime.types",
+    "http.default_type": "application/octet-stream",
+    "http.log_format": [
+      {
+        "key": "http.log_formatXK1BCq0XKQCMEuV",
+        "name": "main",
+        "content": "'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'"
+      }
+    ],
+    "http.access_log": {
+      "key": "http.access_logqwY8npz3ypNjgIA",
+      "name": "main",
+      "path": "/var/log/nginx/access.log"
+    },
+    "http.sendfile": "off",
+    "http.tcp_nopush": "off",
+    "http.keepalive_timeout": 0,
+    "http.gzip": "off",
+    "stream": true,
+    "stream.log_format": [
+      {
+        "key": "stream.log_format8avSYRKrTjzgzcF",
+        "name": "tcp_format",
+        "content": "'$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'"
+      }
+    ],
+    "stream.access_log": {
+      "key": "stream.access_logpuBlWTJXD64QiOL",
+      "name": "tcp_format",
+      "path": "/var/log/nginx/access_stream.log"
+    },
+    "stream.error_log": "/var/log/nginx/error_stream.log"
+  },
+  "server": {
+    "port": 80,
+    "enable": true,
+    "ssl_session_timeout": "5m",
+    "ssl_ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
+    "ssl_protocols": ["TLSv1","TLSv1.1","TLSv1.2"],
+    "ssl_prefer_server_ciphers": "on",
+    "locations": [
+      {
+        "id": "default",
+        "name": "默认",
+        "match": {
+          "path": "/"
+        },
+        "root": "/data/www",
+        "proxy_type": "static",
+        "enable": true
+      }
+    ]
+  },
+  "location": {
+
+  },
+  "addNginx": {
+
+  },
+  "nginxSettings": {}
+}

+ 75 - 75
src/index.css

@@ -1,75 +1,75 @@
-:root {
-  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
-  line-height: 1.5;
-  font-weight: 400;
-
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
-  font-synthesis: none;
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  -webkit-text-size-adjust: 100%;
-}
-
-html,body{
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-}
-
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
-}
-
-body {
-  margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
-}
+:root {
+  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-text-size-adjust: 100%;
+}
+
+html,body{
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 56 - 56
src/main.tsx

@@ -1,56 +1,56 @@
-import React from 'react'
-import ReactDOM, {Root} from 'react-dom/client'
-import './adapter/index.js'
-import App from './App.tsx'
-import './index.css'
-import './styles/index.less'
-import renderWithQiankun from "vite-plugin-qiankun/es/helper";
-
-let root: Root | null
-
-const render = (props: any ={}) => {
-  console.log('[nginx-ui] render', props);
-  const {container} = props;
-  const rootContainer = container ? (container as HTMLElement).querySelector('#nginx_ui_root') : document.getElementById('nginx_ui_root')
-  root = ReactDOM.createRoot(rootContainer as never);
-  root.render(
-    <React.StrictMode>
-      <App />
-    </React.StrictMode>
-  )
-}
-
-const initQianKun = ()=>{
-  renderWithQiankun({
-    bootstrap(){
-      console.log('bootstrap')
-    },
-    mount(props){
-      console.log('[nginx-ui] mount', props)
-      render(props)
-    },
-    unmount(){
-      console.log('unmount')
-      if (!root){
-        return
-      }
-      try {
-        root.unmount()
-        root = null
-      }catch (e) {
-        console.log('[nginx-ui] unmount fail', e)
-      }
-    },
-    update(props){
-      console.log('update', props)
-    }
-  })
-}
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-if (window.__POWERED_BY_QIANKUN__){
-  initQianKun()
-} else {
-  render()
-}
+import React from 'react'
+import ReactDOM, {Root} from 'react-dom/client'
+import './adapter/index.js'
+import App from './App.tsx'
+import './index.css'
+import './styles/index.less'
+import renderWithQiankun from "vite-plugin-qiankun/es/helper";
+
+let root: Root | null
+
+const render = (props: any ={}) => {
+  console.log('[nginx-ui] render', props);
+  const {container} = props;
+  const rootContainer = container ? (container as HTMLElement).querySelector('#nginx_ui_root') : document.getElementById('nginx_ui_root')
+  root = ReactDOM.createRoot(rootContainer as never);
+  root.render(
+    <React.StrictMode>
+      <App />
+    </React.StrictMode>
+  )
+}
+
+const initQianKun = ()=>{
+  renderWithQiankun({
+    bootstrap(){
+      console.log('bootstrap')
+    },
+    mount(props){
+      console.log('[nginx-ui] mount', props)
+      render(props)
+    },
+    unmount(){
+      console.log('unmount')
+      if (!root){
+        return
+      }
+      try {
+        root.unmount()
+        root = null
+      }catch (e) {
+        console.log('[nginx-ui] unmount fail', e)
+      }
+    },
+    update(props){
+      console.log('update', props)
+    }
+  })
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+if (window.__POWERED_BY_QIANKUN__){
+  initQianKun()
+} else {
+  render()
+}

+ 41 - 41
src/models/api.ts

@@ -1,41 +1,41 @@
-/**
- * 后端返回数据的基本格式
- */
-export type BaseResp<T =any> = {
-  code: number
-  msg: string
-  data?: T
-}
-
-/**
- * 虚拟主机,后端,跟前端不一致
- */
-export type IServerHost = {
-  id: number
-  name: string
-  nginxId: number
-  enable?: boolean
-  serverData: string
-  serverConf: string
-  remark: string
-  /**
-   * 是否为TCP/UDP代理
-   */
-  isStream?: boolean
-}
-
-/**
- * 证书信息
- */
-export type INginxCerts = {
-  id: number
-  nginxId: number
-  serviceName: string
-  subjectName?: string
-  hintMsg?: string
-  pem: string
-  key: string
-  createdAt?: string
-  expiresAt?: string
-  remark?: string
-}
+/**
+ * 后端返回数据的基本格式
+ */
+export type BaseResp<T =any> = {
+  code: number
+  msg: string
+  data?: T
+}
+
+/**
+ * 虚拟主机,后端,跟前端不一致
+ */
+export type IServerHost = {
+  id: number
+  name: string
+  nginxId: number
+  enable?: boolean
+  serverData: string
+  serverConf: string
+  remark: string
+  /**
+   * 是否为TCP/UDP代理
+   */
+  isStream?: boolean
+}
+
+/**
+ * 证书信息
+ */
+export type INginxCerts = {
+  id: number
+  nginxId: number
+  serviceName: string
+  subjectName?: string
+  hintMsg?: string
+  pem: string
+  key: string
+  createdAt?: string
+  expiresAt?: string
+  remark?: string
+}

+ 279 - 279
src/models/nginx.ts

@@ -1,279 +1,279 @@
-import {FormColumnType} from "planning-tools";
-import {NgxModuleData} from "../pages/nginx/components/input.ts";
-
-export type INginx = {
-  id: number
-  name: string
-  uid: string
-  /**
-   * 数据目录,所有自定义配置文件都在里面
-   * conf.d stream.d backup certs
-   */
-  dataDir: string
-  /**
-   * nginx的配置文件主目录,及nginx.conf 配置文件所在的目录
-   */
-  nginxDir: string
-  /**
-   * nginx可执行文件
-   */
-  nginxPath?:string
-  isLocal: boolean
-  ipAddr: string
-  port: number
-  user: string
-  password: string
-  httpData: string
-  httpConf: string
-  remark: string
-  /**
-   * 版本信息
-   */
-  versionInfo?: string
-}
-
-/**
- * 虚拟主机或者 TCP/UDP代理
- */
-export type INginxServer = {
-  /**
-   * 唯一标识
-   */
-  id: number
-  /**
-   * 是否是负载均衡
-   */
-  isUpstream?: boolean
-  /**
-   * 是否为TCP/UDP代理
-   */
-  isStream?: boolean
-  nginxId: number
-  enable?: boolean
-  http2?: boolean
-  /**
-   * 配置文件,当前的配置文件
-   */
-  confData?: string
-
-  server_name: string
-  listen: number
-  ssl?: boolean
-  charset?: string
-  access_log?: string
-  error_log?: string
-  /**
-   * 客户端最大的请求体大小,500m 1g
-   */
-  client_max_body_size?: string
-  /**
-   * 证书名称,平台托管的证书名称
-   */
-  certName?: string
-  ssl_certificate?: string
-  ssl_certificate_key?: string
-  /**
-   * eg. 5m 1h
-   */
-  ssl_session_timeout?: string
-  // ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4
-  ssl_ciphers?: string
-  // TLSv1 TLSv1.1 TLSv1.2
-  ssl_protocols?: string[]
-  ssl_prefer_server_ciphers?: 'on'|'off'
-
-  locations?: INginxLocation[]
-  upstreams?: IUpstream[]
-  streams?: INginxStream[]
-  rewrite?: IRewrite
-  remark?: string
-
-  proxy_settings?: NgxModuleData
-  tmp_custom_config?: string
-  gzip?: NgxModuleData
-}
-/**
- * 负载均衡,跟虚拟主机放在一起吧,方便
- */
-export type IUpstream = {
-  name: string
-  /**
-   * weight\backup 不能和 ip_hash 关键字一起使用。
-   * ip_hash 或者weight 轮训
-   */
-  type?:'ip_hash' | 'weight'
-  /**
-   * 是否启用
-   */
-  enable?: boolean
-  servers: {
-    host: string
-    port: number
-    weight?: number
-    /**
-     * down:表示当前的server暂时不参与负载
-     * weight:默认为1.weight越大,负载的权重就越大。
-     * backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
-     */
-    status?: 'down' | 'backup' | 'normal'
-    /**
-     * 最大失败次数,也就是最多进行 3 次尝试,默认为1
-     */
-    max_fails?: number
-    /**
-     * 超时时间,单位秒,默认值是10s
-     */
-    fail_timeout?: number
-  }[]
-}
-
-export type INginxStream = {
-  /**
-   * 唯一索引
-   */
-  key: string
-  listen: number
-  /**
-   * 与被代理服务器建立连接的超时时间为5s
-   */
-  proxy_connect_timeout?: number
-  /**
-   * 获取被代理服务器的响应最大超时时间为10s
-   */
-  proxy_timeout?: number
-  /**
-   * 当被代理的服务器返回错误或超时时,将未返回响应的客户端连接请求传递给upstream中的下一个服务器
-   */
-  proxy_next_upstream?: boolean
-  /**
-   * 总尝试超时时间为10s
-   */
-  proxy_next_upstream_tries?: number
-  /**
-   *  总尝试超时时间为10s
-   */
-  proxy_next_upstream_timeout?: number
-  /**
-   * 开启SO_KEEPALIVE选项进行心跳检测
-   */
-  proxy_socket_keepalive?: boolean
-  /**
-   * proxy_pass
-   */
-  proxy_pass: string
-  /**
-   * 是否启用
-   */
-  enable?: boolean
-}
-
-export type PNginxServer = Partial<INginxServer>
-
-/**
- * 键值对
- */
-export type KeyValue = {
-  name: string
-  value: string
-}
-
-/**
- * nginx 的location配置
- */
-export type INginxLocation = Omit<NgxModuleData, "data"> & {
-  /**
-   * 唯一标识
-   */
-  id: string
-  /**
-   * location的名称
-   */
-  name: string;
-  /**
-   * 匹配规则
-   */
-  match: {
-    path: string
-    regex?: string
-  }
-  index?: string
-  root?: string
-  alias?: string
-  proxy_set_header?: IProxyHeader[]
-  add_header?: IProxyHeader[]
-  proxy_pass?: string
-  // http_502 http_504 http_404 error timeout invalid_header
-  proxy_next_upstream?: string[]
-  //eg. 60s 1m
-  proxy_connect_timeout?: string
-  // 1.1
-  proxy_http_version?: string
-  rewrite?: IRewrite
-
-  proxy_settings?: NgxModuleData
-  gzip?: NgxModuleData
-  tmp_custom_config?: string
-  proxy_type?: 'proxy'| 'static' | 'returnBody' | 'other'
-  /**
-   * 是否为内部路由
-   */
-  internal?: boolean
-  return?: {
-    code: number
-    content: string
-  }
-  /**
-   * 临时数据,表示
-   */
-  __index__?: number
-}
-
-export type PLocation = Partial<INginxLocation>
-
-export type IProxyHeader = {
-  name: string
-  value: string
-}
-
-
-export type IRewrite = {
-  /**
-   * 正则表达式
-   */
-  regex: string
-  /**
-   * 跳转后的内容
-   */
-  replacement: string
-  /**
-   * rewrite支持的flag标记
-   */
-  flag: 'last' | 'break' | 'redirect' | 'permanent'
-}
-
-/**
- * nginx的自动化表单配置
- */
-export type INginxFormConfig = {
-  server: FormColumnType[]
-  location: FormColumnType[]
-  addNginx: FormColumnType[]
-  nginxSettings: FormColumnType[]
-  nginxConf: FormColumnType[]
-  /**
-   * 负载均衡的
-   */
-  upstream: FormColumnType[]
-  stream: FormColumnType[]
-}
-
-/**
- * 给定的初始值模板
- */
-export type INginxFormTemplate = {
-  server: Partial<INginxServer>,
-  location: any,
-  addNginx: any,
-  nginxSettings: any,
-  nginxConf: any
-}
+import {FormColumnType} from "planning-tools";
+import {NgxModuleData} from "../pages/nginx/components/input.ts";
+
+export type INginx = {
+  id: number
+  name: string
+  uid: string
+  /**
+   * 数据目录,所有自定义配置文件都在里面
+   * conf.d stream.d backup certs
+   */
+  dataDir: string
+  /**
+   * nginx的配置文件主目录,及nginx.conf 配置文件所在的目录
+   */
+  nginxDir: string
+  /**
+   * nginx可执行文件
+   */
+  nginxPath?:string
+  isLocal: boolean
+  ipAddr: string
+  port: number
+  user: string
+  password: string
+  httpData: string
+  httpConf: string
+  remark: string
+  /**
+   * 版本信息
+   */
+  versionInfo?: string
+}
+
+/**
+ * 虚拟主机或者 TCP/UDP代理
+ */
+export type INginxServer = {
+  /**
+   * 唯一标识
+   */
+  id: number
+  /**
+   * 是否是负载均衡
+   */
+  isUpstream?: boolean
+  /**
+   * 是否为TCP/UDP代理
+   */
+  isStream?: boolean
+  nginxId: number
+  enable?: boolean
+  http2?: boolean
+  /**
+   * 配置文件,当前的配置文件
+   */
+  confData?: string
+
+  server_name: string
+  listen: number
+  ssl?: boolean
+  charset?: string
+  access_log?: string
+  error_log?: string
+  /**
+   * 客户端最大的请求体大小,500m 1g
+   */
+  client_max_body_size?: string
+  /**
+   * 证书名称,平台托管的证书名称
+   */
+  certName?: string
+  ssl_certificate?: string
+  ssl_certificate_key?: string
+  /**
+   * eg. 5m 1h
+   */
+  ssl_session_timeout?: string
+  // ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4
+  ssl_ciphers?: string
+  // TLSv1 TLSv1.1 TLSv1.2
+  ssl_protocols?: string[]
+  ssl_prefer_server_ciphers?: 'on'|'off'
+
+  locations?: INginxLocation[]
+  upstreams?: IUpstream[]
+  streams?: INginxStream[]
+  rewrite?: IRewrite
+  remark?: string
+
+  proxy_settings?: NgxModuleData
+  tmp_custom_config?: string
+  gzip?: NgxModuleData
+}
+/**
+ * 负载均衡,跟虚拟主机放在一起吧,方便
+ */
+export type IUpstream = {
+  name: string
+  /**
+   * weight\backup 不能和 ip_hash 关键字一起使用。
+   * ip_hash 或者weight 轮训
+   */
+  type?:'ip_hash' | 'weight'
+  /**
+   * 是否启用
+   */
+  enable?: boolean
+  servers: {
+    host: string
+    port: number
+    weight?: number
+    /**
+     * down:表示当前的server暂时不参与负载
+     * weight:默认为1.weight越大,负载的权重就越大。
+     * backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
+     */
+    status?: 'down' | 'backup' | 'normal'
+    /**
+     * 最大失败次数,也就是最多进行 3 次尝试,默认为1
+     */
+    max_fails?: number
+    /**
+     * 超时时间,单位秒,默认值是10s
+     */
+    fail_timeout?: number
+  }[]
+}
+
+export type INginxStream = {
+  /**
+   * 唯一索引
+   */
+  key: string
+  listen: number
+  /**
+   * 与被代理服务器建立连接的超时时间为5s
+   */
+  proxy_connect_timeout?: number
+  /**
+   * 获取被代理服务器的响应最大超时时间为10s
+   */
+  proxy_timeout?: number
+  /**
+   * 当被代理的服务器返回错误或超时时,将未返回响应的客户端连接请求传递给upstream中的下一个服务器
+   */
+  proxy_next_upstream?: boolean
+  /**
+   * 总尝试超时时间为10s
+   */
+  proxy_next_upstream_tries?: number
+  /**
+   *  总尝试超时时间为10s
+   */
+  proxy_next_upstream_timeout?: number
+  /**
+   * 开启SO_KEEPALIVE选项进行心跳检测
+   */
+  proxy_socket_keepalive?: boolean
+  /**
+   * proxy_pass
+   */
+  proxy_pass: string
+  /**
+   * 是否启用
+   */
+  enable?: boolean
+}
+
+export type PNginxServer = Partial<INginxServer>
+
+/**
+ * 键值对
+ */
+export type KeyValue = {
+  name: string
+  value: string
+}
+
+/**
+ * nginx 的location配置
+ */
+export type INginxLocation = Omit<NgxModuleData, "data"> & {
+  /**
+   * 唯一标识
+   */
+  id: string
+  /**
+   * location的名称
+   */
+  name: string;
+  /**
+   * 匹配规则
+   */
+  match: {
+    path: string
+    regex?: string
+  }
+  index?: string
+  root?: string
+  alias?: string
+  proxy_set_header?: IProxyHeader[]
+  add_header?: IProxyHeader[]
+  proxy_pass?: string
+  // http_502 http_504 http_404 error timeout invalid_header
+  proxy_next_upstream?: string[]
+  //eg. 60s 1m
+  proxy_connect_timeout?: string
+  // 1.1
+  proxy_http_version?: string
+  rewrite?: IRewrite
+
+  proxy_settings?: NgxModuleData
+  gzip?: NgxModuleData
+  tmp_custom_config?: string
+  proxy_type?: 'proxy'| 'static' | 'returnBody' | 'other'
+  /**
+   * 是否为内部路由
+   */
+  internal?: boolean
+  return?: {
+    code: number
+    content: string
+  }
+  /**
+   * 临时数据,表示
+   */
+  __index__?: number
+}
+
+export type PLocation = Partial<INginxLocation>
+
+export type IProxyHeader = {
+  name: string
+  value: string
+}
+
+
+export type IRewrite = {
+  /**
+   * 正则表达式
+   */
+  regex: string
+  /**
+   * 跳转后的内容
+   */
+  replacement: string
+  /**
+   * rewrite支持的flag标记
+   */
+  flag: 'last' | 'break' | 'redirect' | 'permanent'
+}
+
+/**
+ * nginx的自动化表单配置
+ */
+export type INginxFormConfig = {
+  server: FormColumnType[]
+  location: FormColumnType[]
+  addNginx: FormColumnType[]
+  nginxSettings: FormColumnType[]
+  nginxConf: FormColumnType[]
+  /**
+   * 负载均衡的
+   */
+  upstream: FormColumnType[]
+  stream: FormColumnType[]
+}
+
+/**
+ * 给定的初始值模板
+ */
+export type INginxFormTemplate = {
+  server: Partial<INginxServer>,
+  location: any,
+  addNginx: any,
+  nginxSettings: any,
+  nginxConf: any
+}

+ 52 - 52
src/pages/nginx/certs/index.less

@@ -1,52 +1,52 @@
-.cert-page{
-  padding: 0 10px;
-  .cert-list{
-    display: flex;
-    flex-direction: column;
-  }
-  .cert-tags{
-    padding: 10px;
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-    .ant-tag{
-      margin-bottom: 5px;
-      cursor: pointer;
-    }
-  }
-
-}
-.cert-edit-drawer{
-  .ant-drawer-header{
-    padding: 5px 10px;
-  }
-  .ant-drawer-body{
-    padding: 10px;
-
-    .ant-picker{
-      min-width: 60%;
-    }
-  }
-
-  .cert-data{
-    max-width: 900px;
-    h5{
-      font-weight: bold;
-      font-size: 16px;
-    }
-  }
-  .inline-item{
-    display: flex;
-    flex-direction: row;
-    .ant-form-item{
-      flex: 1;
-      margin-right: 10px;
-    }
-  }
-  .footer-item{
-    padding-left: 16%;
-    .ant-btn+.ant-btn{
-      margin-left: 10px;
-    }
-  }
-}
+.cert-page{
+  padding: 0 10px;
+  .cert-list{
+    display: flex;
+    flex-direction: column;
+  }
+  .cert-tags{
+    padding: 10px;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    .ant-tag{
+      margin-bottom: 5px;
+      cursor: pointer;
+    }
+  }
+
+}
+.cert-edit-drawer{
+  .ant-drawer-header{
+    padding: 5px 10px;
+  }
+  .ant-drawer-body{
+    padding: 10px;
+
+    .ant-picker{
+      min-width: 60%;
+    }
+  }
+
+  .cert-data{
+    max-width: 900px;
+    h5{
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
+  .inline-item{
+    display: flex;
+    flex-direction: row;
+    .ant-form-item{
+      flex: 1;
+      margin-right: 10px;
+    }
+  }
+  .footer-item{
+    padding-left: 16%;
+    .ant-btn+.ant-btn{
+      margin-left: 10px;
+    }
+  }
+}

+ 249 - 249
src/pages/nginx/certs/index.tsx

@@ -1,249 +1,249 @@
-import {NginxApis} from "../../../api/nginx.ts";
-import {useAppSelector} from "../../../store";
-import {useEffect, useState} from "react";
-import {Button, Drawer, Form, Input, Modal, Table, Tooltip, Upload} from "antd";
-import {
-  DeleteOutlined,
-  EditOutlined,
-  ImportOutlined,
-  PlusOutlined, QuestionOutlined,
-  SyncOutlined,
-  UploadOutlined
-} from "@ant-design/icons";
-
-import './index.less'
-import {INginxCerts} from "../../../models/api.ts";
-import {RcFile} from "antd/es/upload";
-import {isNull, Message} from "planning-tools";
-import {ModalStaticFunctions} from "antd/es/modal/confirm";
-
-/**
- * 证书管理
- * @constructor
- */
-export const NginxCerts = () => {
-    const nginx = useAppSelector(state => state.nginx.current)
-
-    const [loading, setLoading] = useState(false)
-    const [certs, setCerts] = useState<INginxCerts[]>([])
-    const [cert, setCert] = useState<Partial<INginxCerts>>()
-    const [modal,contextHolder] = Modal.useModal()
-
-
-  const [form] = Form.useForm()
-
-    const onBeforeUpload = (name: 'pem' | 'key', file: RcFile) => {
-        console.log('onBeforeUpload', name, file.name)
-        file.text().then(v=>{
-            const data = {...cert, [name]: v };
-            setCert(data as INginxCerts)
-            form.setFieldsValue(data)
-        })
-
-        return false
-    }
-
-    const fetchList = () => {
-        if (!nginx?.id) {
-            return
-        }
-        setLoading(true)
-        NginxApis.getCerts(nginx.id)
-            .then(({data}) => {
-                const content = data.data;
-                if (!content) {
-                    setCerts([])
-                }else {
-                  setCerts(data.data)
-                }
-            })
-            .finally(() => {
-                setLoading(false)
-            })
-    }
-
-    const syncFromDisk = ()=>{
-      if (!nginx?.id){
-        return
-      }
-      setLoading(true)
-      NginxApis.syncCerts(nginx.id)
-        .then(() => {
-          fetchList()
-        })
-        .finally(() => {
-          setLoading(false)
-        })
-    }
-
-    const onAddData = ()=>{
-        setCert({})
-        form.resetFields()
-    }
-
-  const onEditCert = (data: INginxCerts)=>{
-    const fields = { ...data }
-    setCert(fields)
-    form.setFieldsValue(fields)
-  }
-
-    const onSubmitData = async ()=>{
-        if (!nginx?.id){
-          Message.warning('缓存数据异常,请退出到首页重新进去nginx实例配置页面。')
-            return
-        }
-        const values = await form.validateFields()
-        console.log('values',values);
-        setLoading(true);
-        const postData = { ...cert, ...values}
-        postData.nginxId = nginx.id
-        NginxApis.saveCerts(nginx.id, postData)
-            .then(({data})=>{
-                console.log('data',data);
-                Message.success('保存成功!');
-                setCert(undefined)
-                fetchList();
-            })
-            .finally(()=>{
-                setLoading(false)
-            })
-    }
-
-
-    useEffect(() => {
-        fetchList()
-    }, [])
-
-    return (<div className="page cert-page">
-        <div className="page-header">
-            <Button type="primary" loading={loading} onClick={fetchList} icon={<SyncOutlined/>}/>
-            <Button onClick={onAddData} icon={<PlusOutlined/>}/>
-            <div style={{flex:1}} />
-          <Tooltip placement="left" title="从数据目录中导入,适用于初始化;如果数据库中已存在,会覆盖,请谨慎处理">
-            <Button danger loading={loading} onClick={syncFromDisk} icon={<ImportOutlined/>}></Button>
-          </Tooltip>
-        </div>
-        <div className="page-container cert-list">
-          <Table rowKey="id" dataSource={certs} pagination={false}>
-            <Table.Column
-              title="名称"
-              dataIndex="serviceName"
-            />
-            <Table.Column
-              title="域名"
-              dataIndex="subjectName"
-              render={(v,data: INginxCerts)=>{
-                return (<>
-                  {v || '--'}
-                  { data.hintMsg ? <Tooltip title={data.hintMsg}><QuestionOutlined /></Tooltip> : null }
-                </>)
-              }}
-            />
-            <Table.Column title="添加时间" dataIndex="createdAt" />
-            <Table.Column title="过期时间" dataIndex="expiresAt" />
-            <Table.Column title="备注" dataIndex="remark" />
-            <Table.Column title="操作" dataIndex="ops"
-                          width={120}
-                          render={(_,data: INginxCerts)=>(<>
-                            <DelButton onRefresh={fetchList} cert={data} nginxId={nginx?.id || 0} modal={modal} />
-                            <Button onClick={()=>onEditCert(data)} type="link" icon={<EditOutlined />}></Button>
-            </>)} />
-          </Table>
-          <Drawer open={!!cert}
-                  width={750}
-                  className="cert-edit-drawer"
-                  onClose={()=>setCert(undefined)}
-                  destroyOnClose
-                  title={isNull(cert?.id) ?'添加证书': '编辑证书'}>
-
-            <div className="cert-data">
-              <Form form={form} initialValues={cert} labelCol={{span: 4}}>
-                <Form.Item name="serviceName"
-                           rules={[{required: true, message: '请输入域名或者名称,唯一不可重复'}]}
-                           label="域名">
-                  <Input />
-                </Form.Item>
-                <div className="inline-item" >
-                  <Form.Item name="pem"
-                             rules={[{required: true, message: '请输入或者选择证书'}]}
-                             label="pem证书">
-                    <Input.TextArea rows={8}/>
-
-                  </Form.Item>
-                  <Upload beforeUpload={(file) => onBeforeUpload("pem", file)}
-                          showUploadList={false}
-                          accept=".pem">
-                    <Button icon={<UploadOutlined/>}></Button>
-                  </Upload>
-                </div>
-                <div className="inline-item" >
-                  <Form.Item name="key"
-                             rules={[{required: true, message: '请输入或者选择私钥'}]}
-                             label="私钥">
-                    <Input.TextArea rows={8} />
-
-                  </Form.Item>
-                  <Upload beforeUpload={(file) => onBeforeUpload("key", file)}
-                          showUploadList={false}
-                          accept=".key">
-                    <Button icon={<UploadOutlined/>}></Button>
-                  </Upload>
-                </div>
-
-                <Form.Item name="remark" label="备注">
-                  <Input.TextArea />
-                </Form.Item>
-                <Form.Item className="footer-item">
-                  <Button onClick={()=>setCert(undefined)}>取消</Button>
-                  <Button loading={loading} onClick={onSubmitData} type="primary">保存</Button>
-                </Form.Item>
-              </Form>
-            </div>
-          </Drawer>
-        </div>
-      {contextHolder}
-
-    </div>)
-}
-type IProps = {
-    cert: INginxCerts
-    nginxId: number
-    onRefresh: () => void
-  modal: Omit<ModalStaticFunctions, "warn">
-}
-const DelButton = ({cert, nginxId, onRefresh, modal}: IProps)=>{
-
-    const [loading,setLoading] = useState(false)
-
-    const onDel = (e: any)=>{
-        e.preventDefault()
-        modal.confirm({
-            title: '警告',
-            content: '您确定要删除该证书信息吗?删除操作不可恢复,请谨慎操作.',
-            okType: 'danger',
-            cancelText: '取消',
-            okText: '确定',
-            onOk: ()=>{
-                setLoading(true)
-                NginxApis.delCerts(nginxId, cert.id)
-                    .then(()=>{
-                        onRefresh?.()
-                    })
-                    .finally(()=>{
-                        setLoading(false)
-                    })
-            }
-        })
-    }
-
-
-
-    return <>
-        <Button onClick={onDel}
-                loading={loading}
-                type="text"
-                danger
-                icon={<DeleteOutlined />}
-        />
-    </>
-}
+import {NginxApis} from "../../../api/nginx.ts";
+import {useAppSelector} from "../../../store";
+import {useEffect, useState} from "react";
+import {Button, Drawer, Form, Input, Modal, Table, Tooltip, Upload} from "antd";
+import {
+  DeleteOutlined,
+  EditOutlined,
+  ImportOutlined,
+  PlusOutlined, QuestionOutlined,
+  SyncOutlined,
+  UploadOutlined
+} from "@ant-design/icons";
+
+import './index.less'
+import {INginxCerts} from "../../../models/api.ts";
+import {RcFile} from "antd/es/upload";
+import {isNull, Message} from "planning-tools";
+import {ModalStaticFunctions} from "antd/es/modal/confirm";
+
+/**
+ * 证书管理
+ * @constructor
+ */
+export const NginxCerts = () => {
+    const nginx = useAppSelector(state => state.nginx.current)
+
+    const [loading, setLoading] = useState(false)
+    const [certs, setCerts] = useState<INginxCerts[]>([])
+    const [cert, setCert] = useState<Partial<INginxCerts>>()
+    const [modal,contextHolder] = Modal.useModal()
+
+
+  const [form] = Form.useForm()
+
+    const onBeforeUpload = (name: 'pem' | 'key', file: RcFile) => {
+        console.log('onBeforeUpload', name, file.name)
+        file.text().then(v=>{
+            const data = {...cert, [name]: v };
+            setCert(data as INginxCerts)
+            form.setFieldsValue(data)
+        })
+
+        return false
+    }
+
+    const fetchList = () => {
+        if (!nginx?.id) {
+            return
+        }
+        setLoading(true)
+        NginxApis.getCerts(nginx.id)
+            .then(({data}) => {
+                const content = data.data;
+                if (!content) {
+                    setCerts([])
+                }else {
+                  setCerts(data.data)
+                }
+            })
+            .finally(() => {
+                setLoading(false)
+            })
+    }
+
+    const syncFromDisk = ()=>{
+      if (!nginx?.id){
+        return
+      }
+      setLoading(true)
+      NginxApis.syncCerts(nginx.id)
+        .then(() => {
+          fetchList()
+        })
+        .finally(() => {
+          setLoading(false)
+        })
+    }
+
+    const onAddData = ()=>{
+        setCert({})
+        form.resetFields()
+    }
+
+  const onEditCert = (data: INginxCerts)=>{
+    const fields = { ...data }
+    setCert(fields)
+    form.setFieldsValue(fields)
+  }
+
+    const onSubmitData = async ()=>{
+        if (!nginx?.id){
+          Message.warning('缓存数据异常,请退出到首页重新进去nginx实例配置页面。')
+            return
+        }
+        const values = await form.validateFields()
+        console.log('values',values);
+        setLoading(true);
+        const postData = { ...cert, ...values}
+        postData.nginxId = nginx.id
+        NginxApis.saveCerts(nginx.id, postData)
+            .then(({data})=>{
+                console.log('data',data);
+                Message.success('保存成功!');
+                setCert(undefined)
+                fetchList();
+            })
+            .finally(()=>{
+                setLoading(false)
+            })
+    }
+
+
+    useEffect(() => {
+        fetchList()
+    }, [])
+
+    return (<div className="page cert-page">
+        <div className="page-header">
+            <Button type="primary" loading={loading} onClick={fetchList} icon={<SyncOutlined/>}/>
+            <Button onClick={onAddData} icon={<PlusOutlined/>}/>
+            <div style={{flex:1}} />
+          <Tooltip placement="left" title="从数据目录中导入,适用于初始化;如果数据库中已存在,会覆盖,请谨慎处理">
+            <Button danger loading={loading} onClick={syncFromDisk} icon={<ImportOutlined/>}></Button>
+          </Tooltip>
+        </div>
+        <div className="page-container cert-list">
+          <Table rowKey="id" dataSource={certs} pagination={false}>
+            <Table.Column
+              title="名称"
+              dataIndex="serviceName"
+            />
+            <Table.Column
+              title="域名"
+              dataIndex="subjectName"
+              render={(v,data: INginxCerts)=>{
+                return (<>
+                  {v || '--'}
+                  { data.hintMsg ? <Tooltip title={data.hintMsg}><QuestionOutlined /></Tooltip> : null }
+                </>)
+              }}
+            />
+            <Table.Column title="添加时间" dataIndex="createdAt" />
+            <Table.Column title="过期时间" dataIndex="expiresAt" />
+            <Table.Column title="备注" dataIndex="remark" />
+            <Table.Column title="操作" dataIndex="ops"
+                          width={120}
+                          render={(_,data: INginxCerts)=>(<>
+                            <DelButton onRefresh={fetchList} cert={data} nginxId={nginx?.id || 0} modal={modal} />
+                            <Button onClick={()=>onEditCert(data)} type="link" icon={<EditOutlined />}></Button>
+            </>)} />
+          </Table>
+          <Drawer open={!!cert}
+                  width={750}
+                  className="cert-edit-drawer"
+                  onClose={()=>setCert(undefined)}
+                  destroyOnClose
+                  title={isNull(cert?.id) ?'添加证书': '编辑证书'}>
+
+            <div className="cert-data">
+              <Form form={form} initialValues={cert} labelCol={{span: 4}}>
+                <Form.Item name="serviceName"
+                           rules={[{required: true, message: '请输入域名或者名称,唯一不可重复'}]}
+                           label="域名">
+                  <Input />
+                </Form.Item>
+                <div className="inline-item" >
+                  <Form.Item name="pem"
+                             rules={[{required: true, message: '请输入或者选择证书'}]}
+                             label="pem证书">
+                    <Input.TextArea rows={8}/>
+
+                  </Form.Item>
+                  <Upload beforeUpload={(file) => onBeforeUpload("pem", file)}
+                          showUploadList={false}
+                          accept=".pem">
+                    <Button icon={<UploadOutlined/>}></Button>
+                  </Upload>
+                </div>
+                <div className="inline-item" >
+                  <Form.Item name="key"
+                             rules={[{required: true, message: '请输入或者选择私钥'}]}
+                             label="私钥">
+                    <Input.TextArea rows={8} />
+
+                  </Form.Item>
+                  <Upload beforeUpload={(file) => onBeforeUpload("key", file)}
+                          showUploadList={false}
+                          accept=".key">
+                    <Button icon={<UploadOutlined/>}></Button>
+                  </Upload>
+                </div>
+
+                <Form.Item name="remark" label="备注">
+                  <Input.TextArea />
+                </Form.Item>
+                <Form.Item className="footer-item">
+                  <Button onClick={()=>setCert(undefined)}>取消</Button>
+                  <Button loading={loading} onClick={onSubmitData} type="primary">保存</Button>
+                </Form.Item>
+              </Form>
+            </div>
+          </Drawer>
+        </div>
+      {contextHolder}
+
+    </div>)
+}
+type IProps = {
+    cert: INginxCerts
+    nginxId: number
+    onRefresh: () => void
+  modal: Omit<ModalStaticFunctions, "warn">
+}
+const DelButton = ({cert, nginxId, onRefresh, modal}: IProps)=>{
+
+    const [loading,setLoading] = useState(false)
+
+    const onDel = (e: any)=>{
+        e.preventDefault()
+        modal.confirm({
+            title: '警告',
+            content: '您确定要删除该证书信息吗?删除操作不可恢复,请谨慎操作.',
+            okType: 'danger',
+            cancelText: '取消',
+            okText: '确定',
+            onOk: ()=>{
+                setLoading(true)
+                NginxApis.delCerts(nginxId, cert.id)
+                    .then(()=>{
+                        onRefresh?.()
+                    })
+                    .finally(()=>{
+                        setLoading(false)
+                    })
+            }
+        })
+    }
+
+
+
+    return <>
+        <Button onClick={onDel}
+                loading={loading}
+                type="text"
+                danger
+                icon={<DeleteOutlined />}
+        />
+    </>
+}

+ 54 - 54
src/pages/nginx/components/EditNginxBtn.tsx

@@ -1,54 +1,54 @@
-/**
- * @author tuonian
- * @date 2023/6/30
- */
-import {INginx} from "../../../models/nginx.ts";
-import {NginxActions} from "../../../store/slice/nginx.ts";
-import {useState} from "react";
-import {EditOutlined} from "@ant-design/icons";
-import {Button} from "antd";
-import {NginxApis} from "../../../api/nginx.ts";
-import {Notify} from "planning-tools";
-import {useAppDispatch} from "../../../store";
-import {useNavigate} from "react-router";
-import {nginxPrefix} from "../../../routes/routes.tsx";
-
-type IProps = {
-  nginx: INginx
-}
-export const EditNginxBtn = ({nginx}: IProps)=>{
-
-  const [loading,setLoading] = useState(false)
-  const dispatch = useAppDispatch()
-  const navigate = useNavigate()
-
-  const toNginx = ()=>{
-    setLoading(true);
-    NginxApis.getNginx(nginx.id)
-      .then(({data})=>{
-        const respData = data.data;
-        if (!respData){
-          Notify.warn('查询失败,请重试!');
-          return
-        }
-        console.log('getNginx', data)
-        dispatch(NginxActions.setCurrent({
-            nginx,
-            servers: respData.servers
-        }))
-        navigate(nginxPrefix(nginx.id))
-      })
-      .catch(e=>{
-        Notify.warn(e.msg || e.message)
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-
-
-  }
-
-  return (
-    <Button loading={loading} onClick={()=>toNginx()} type="link" icon={<EditOutlined />} />
-  )
-}
+/**
+ * @author tuonian
+ * @date 2023/6/30
+ */
+import {INginx} from "../../../models/nginx.ts";
+import {NginxActions} from "../../../store/slice/nginx.ts";
+import {useState} from "react";
+import {EditOutlined} from "@ant-design/icons";
+import {Button} from "antd";
+import {NginxApis} from "../../../api/nginx.ts";
+import {Notify} from "planning-tools";
+import {useAppDispatch} from "../../../store";
+import {useNavigate} from "react-router";
+import {nginxPrefix} from "../../../routes/routes.tsx";
+
+type IProps = {
+  nginx: INginx
+}
+export const EditNginxBtn = ({nginx}: IProps)=>{
+
+  const [loading,setLoading] = useState(false)
+  const dispatch = useAppDispatch()
+  const navigate = useNavigate()
+
+  const toNginx = ()=>{
+    setLoading(true);
+    NginxApis.getNginx(nginx.id)
+      .then(({data})=>{
+        const respData = data.data;
+        if (!respData){
+          Notify.warn('查询失败,请重试!');
+          return
+        }
+        console.log('getNginx', data)
+        dispatch(NginxActions.setCurrent({
+            nginx,
+            servers: respData.servers
+        }))
+        navigate(nginxPrefix(nginx.id))
+      })
+      .catch(e=>{
+        Notify.warn(e.msg || e.message)
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+
+
+  }
+
+  return (
+    <Button loading={loading} onClick={()=>toNginx()} type="link" icon={<EditOutlined />} />
+  )
+}

+ 97 - 97
src/pages/nginx/components/StopStartButton.tsx

@@ -1,97 +1,97 @@
-import {useEffect, useState} from "react";
-import {Button, Modal, Tag} from "antd";
-import {isNull, Message} from "planning-tools";
-import {NginxApis} from "../../../api/nginx.ts";
-import {useAppSelector} from "../../../store";
-
-/**
- *
- * @constructor
- */
-export const StopStartButton = () => {
-
-    const [isRun,setIsRun] = useState<boolean>()
-    const [loading,setLoading] = useState(false)
-    const [modal,contextHolder] = Modal.useModal()
-
-    const nginx = useAppSelector(state => state.nginx.current)
-
-    const fetchStatus = () => {
-        if (!nginx){
-            return
-        }
-        setLoading(true)
-        NginxApis.status(nginx.id)
-            .then(({data})=>{
-                setIsRun(data.data)
-                console.log('status', data)
-                if (!data.msg){
-                    return
-                }
-                if (data.data){
-                    Message.success(data.msg)
-                }else {
-                    Message.warning(data.msg)
-                }
-            })
-            .finally(()=>{
-                setLoading(false)
-            })
-    }
-
-
-    const postStartOrStopApi = ()=>{
-        if (!nginx){
-            return
-        }
-        setLoading(true)
-        const request = isRun ? NginxApis.stopNginx(nginx.id) : NginxApis.startNginx(nginx.id);
-        request.then(({data})=>{
-            console.log('data', data);
-            setIsRun(data.data);
-            if (data.msg){
-                Message.warning(data.msg)
-            }
-        }).finally(()=>{
-            setLoading(false)
-        })
-    }
-    const onStartOrStop  = () => {
-        if (isNull(isRun)){
-            fetchStatus()
-            return
-        }
-        modal.confirm({
-            type: 'warning',
-            title: `您确定要${isRun ? '停止' : '启动'}nginx服务吗?`,
-            okType: 'danger',
-            okText: '确定',
-            cancelText: '取消',
-            onOk: ()=>{
-                postStartOrStopApi()
-            }
-        })
-    }
-
-    useEffect(()=>{
-        fetchStatus()
-    },[])
-
-    if (!nginx){
-        return null
-    }
-
-    return (<>
-      <span>Nginx:</span>
-      <Tag color={isNull(isRun) ? 'grey': isRun ? 'green': 'red'}>{isNull(isRun) ? '未知': isRun ? '运行中':'已停止'}</Tag>
-        <Button type={ isRun?'default' : 'primary'}
-                onClick={onStartOrStop}
-                hidden={isNull(isRun)}
-                size="small"
-                danger={isRun}
-                loading={loading}>
-            { isRun ? '停止':'启动'}
-        </Button>
-        {contextHolder}
-        </>)
-}
+import {useEffect, useState} from "react";
+import {Button, Modal, Tag} from "antd";
+import {isNull, Message} from "planning-tools";
+import {NginxApis} from "../../../api/nginx.ts";
+import {useAppSelector} from "../../../store";
+
+/**
+ *
+ * @constructor
+ */
+export const StopStartButton = () => {
+
+    const [isRun,setIsRun] = useState<boolean>()
+    const [loading,setLoading] = useState(false)
+    const [modal,contextHolder] = Modal.useModal()
+
+    const nginx = useAppSelector(state => state.nginx.current)
+
+    const fetchStatus = () => {
+        if (!nginx){
+            return
+        }
+        setLoading(true)
+        NginxApis.status(nginx.id)
+            .then(({data})=>{
+                setIsRun(data.data)
+                console.log('status', data)
+                if (!data.msg){
+                    return
+                }
+                if (data.data){
+                    Message.success(data.msg)
+                }else {
+                    Message.warning(data.msg)
+                }
+            })
+            .finally(()=>{
+                setLoading(false)
+            })
+    }
+
+
+    const postStartOrStopApi = ()=>{
+        if (!nginx){
+            return
+        }
+        setLoading(true)
+        const request = isRun ? NginxApis.stopNginx(nginx.id) : NginxApis.startNginx(nginx.id);
+        request.then(({data})=>{
+            console.log('data', data);
+            setIsRun(data.data);
+            if (data.msg){
+                Message.warning(data.msg)
+            }
+        }).finally(()=>{
+            setLoading(false)
+        })
+    }
+    const onStartOrStop  = () => {
+        if (isNull(isRun)){
+            fetchStatus()
+            return
+        }
+        modal.confirm({
+            type: 'warning',
+            title: `您确定要${isRun ? '停止' : '启动'}nginx服务吗?`,
+            okType: 'danger',
+            okText: '确定',
+            cancelText: '取消',
+            onOk: ()=>{
+                postStartOrStopApi()
+            }
+        })
+    }
+
+    useEffect(()=>{
+        fetchStatus()
+    },[])
+
+    if (!nginx){
+        return null
+    }
+
+    return (<>
+      <span>Nginx:</span>
+      <Tag color={isNull(isRun) ? 'grey': isRun ? 'green': 'red'}>{isNull(isRun) ? '未知': isRun ? '运行中':'已停止'}</Tag>
+        <Button type={ isRun?'default' : 'primary'}
+                onClick={onStartOrStop}
+                hidden={isNull(isRun)}
+                size="small"
+                danger={isRun}
+                loading={loading}>
+            { isRun ? '停止':'启动'}
+        </Button>
+        {contextHolder}
+        </>)
+}

+ 39 - 39
src/pages/nginx/components/auth/config.json

@@ -1,39 +1,39 @@
-{
-  "form": [
-    {
-      "title": "是否启用",
-      "key": "auth_request_on",
-      "required": false,
-      "type": "switch",
-      "cascade": {
-        "true": [
-          {
-            "title": "auth_request_uri",
-            "key": "auth_request_uri",
-            "type": "string",
-            "placeholder": "输入鉴权的路由"
-          }
-        ]
-      }
-    },
-    {
-      "title": "auth_request_set",
-      "key": "auth_request_set",
-      "required": false,
-      "type": "array",
-      "items": [
-        {
-          "key": "name",
-          "title": "变量",
-          "type": "string"
-        },
-        {
-          "key": "value",
-          "title": "变量值",
-          "type": "string"
-        }
-      ],
-      "description": "Sets the request variable to the given value after the authorization request completes. The value may contain variables from the authorization request, such as $upstream_http_*."
-    }
-  ]
-}
+{
+  "form": [
+    {
+      "title": "是否启用",
+      "key": "auth_request_on",
+      "required": false,
+      "type": "switch",
+      "cascade": {
+        "true": [
+          {
+            "title": "auth_request_uri",
+            "key": "auth_request_uri",
+            "type": "string",
+            "placeholder": "输入鉴权的路由"
+          }
+        ]
+      }
+    },
+    {
+      "title": "auth_request_set",
+      "key": "auth_request_set",
+      "required": false,
+      "type": "array",
+      "items": [
+        {
+          "key": "name",
+          "title": "变量",
+          "type": "string"
+        },
+        {
+          "key": "value",
+          "title": "变量值",
+          "type": "string"
+        }
+      ],
+      "description": "Sets the request variable to the given value after the authorization request completes. The value may contain variables from the authorization request, such as $upstream_http_*."
+    }
+  ]
+}

+ 10 - 10
src/pages/nginx/components/auth/index.less

@@ -1,10 +1,10 @@
-.gzip-input{
-  margin-right: 10px;
-}
-
-.gzip-popover{
-  .auto-form{
-    min-width: 450px;
-    width: 550px;
-  }
-}
+.gzip-input{
+  margin-right: 10px;
+}
+
+.gzip-popover{
+  .auto-form{
+    min-width: 450px;
+    width: 550px;
+  }
+}

+ 40 - 40
src/pages/nginx/components/auth/index.tsx

@@ -1,40 +1,40 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {AutoTypeInputProps} from 'planning-tools'
-import './index.less'
-import config from './config.json'
-import {IContentProps, NgxBasicInput, registerInput} from "../basic";
-import {KeyValue} from "../../../../models/nginx.ts";
-
-export const AuthInput = ({...props}: AutoTypeInputProps)=>{
-
-  const ShowContent = ({data}: IContentProps)=>{
-    return (<span>{data.auth_request_on ? data.auth_request_uri : '不启用'}</span>)
-  }
-
-  const renderLines = (values: any)=>{
-    const lines: string[] = []
-    if (!values.auth_request_on){
-      return lines
-    }
-    lines.push(`auth_request  ${values.auth_request_uri};`)
-    if (Array.isArray(values.auth_request_set)){
-      values.auth_request_set.forEach((item: KeyValue)=>{
-        lines.push(`auth_request_set  ${item.name}  ${item.value};`)
-      })
-    }
-    return lines;
-  }
-
-  return <NgxBasicInput
-    {...props}
-    columns={config.form}
-    renderLines={renderLines}
-    content={ShowContent}
-    />
-}
-
-registerInput('auth', AuthInput)
-
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {AutoTypeInputProps} from 'planning-tools'
+import './index.less'
+import config from './config.json'
+import {IContentProps, NgxBasicInput, registerInput} from "../basic";
+import {KeyValue} from "../../../../models/nginx.ts";
+
+export const AuthInput = ({...props}: AutoTypeInputProps)=>{
+
+  const ShowContent = ({data}: IContentProps)=>{
+    return (<span>{data.auth_request_on ? data.auth_request_uri : '不启用'}</span>)
+  }
+
+  const renderLines = (values: any)=>{
+    const lines: string[] = []
+    if (!values.auth_request_on){
+      return lines
+    }
+    lines.push(`auth_request  ${values.auth_request_uri};`)
+    if (Array.isArray(values.auth_request_set)){
+      values.auth_request_set.forEach((item: KeyValue)=>{
+        lines.push(`auth_request_set  ${item.name}  ${item.value};`)
+      })
+    }
+    return lines;
+  }
+
+  return <NgxBasicInput
+    {...props}
+    columns={config.form}
+    renderLines={renderLines}
+    content={ShowContent}
+    />
+}
+
+registerInput('auth', AuthInput)
+

+ 19 - 19
src/pages/nginx/components/basic/index.less

@@ -1,19 +1,19 @@
-.popover-input{
-  margin-right: 10px;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.popover-popover{
-  .auto-form{
-    min-width: 450px;
-    width: 550px;
-  }
-  .form-btns{
-    padding-left: 25%;
-    .ant-btn+.ant-btn{
-      margin-left: 10px;
-    }
-  }
-}
+.popover-input{
+  margin-right: 10px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.popover-popover{
+  .auto-form{
+    min-width: 450px;
+    width: 550px;
+  }
+  .form-btns{
+    padding-left: 25%;
+    .ant-btn+.ant-btn{
+      margin-left: 10px;
+    }
+  }
+}

+ 108 - 108
src/pages/nginx/components/basic/index.tsx

@@ -1,108 +1,108 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {AdvanceInputConfigs, AutoForm, AutoTypeInputProps, FormColumnType} from 'planning-tools'
-import {Button, FormInstance, Popover} from "antd";
-import {EditOutlined} from "@ant-design/icons";
-import React, {useEffect, useState} from "react";
-import {AutoFormFooterProps} from "planning-tools/dist/esm/Components/AutoForm/form";
-import {NgxModuleData} from "../input.ts";
-import './index.less'
-
-export type OnChange = (values: any) => void
-
-export type IContentProps<T = any> = {
-  data: T
-  onChange: OnChange
-}
-
-type IProps = {
-  content: React.FC<IContentProps>
-  columns?: FormColumnType[]
-  renderLines: (values: any) => string[]
-  renderHttpLines?: (values: any) =>string[]
-  overlayClassName?: string
-  labelCol?: number
-}
-export const NgxBasicInput = (
-  {
-    content: ContentComp,
-    columns = [],
-    renderLines,
-      renderHttpLines,
-    overlayClassName,
-    labelCol = 6,
-    value, onChange
-  }: IProps & AutoTypeInputProps) => {
-
-  const [data, setData] = useState<any>({})
-  const [open, setOpen] = useState(false)
-
-  useEffect(() => {
-    if (value?.data) {
-      setData(value.data || {})
-    }
-  }, [value])
-
-  const triggerChange = (values: any) => {
-    const lines = renderLines(values)
-    onChange?.({
-      data: values,
-      lines: lines,
-      http: renderHttpLines?.(values) || []
-    } as NgxModuleData)
-  }
-
-  const onValuesChange = (values: any) => {
-    const current = {...data, ...values}
-    setData(current)
-    setOpen(false)
-    triggerChange(current)
-  }
-
-  const onSubmitData = async (form: FormInstance | null) => {
-    if (!form) {
-      return
-    }
-    const values = await form.validateFields();
-    console.log('values', values);
-    onValuesChange(values)
-  }
-
-  const renderFormFooter = ({formRef}: AutoFormFooterProps) => {
-    return (<div className="form-btns">
-      <Button onClick={() => onSubmitData(formRef.current as never)} type="primary">保存</Button>
-      <Button onClick={() => setOpen(false)}>取消</Button>
-    </div>)
-  }
-
-  const renderForm = () => {
-    return (<AutoForm columns={columns}
-                      formProps={{
-                        labelCol: {span: labelCol}
-                      }}
-                      onlyFields={true}
-                      footer={renderFormFooter}
-                      data={data}/>)
-  }
-
-  return (<div className="popover-input">
-    <ContentComp data={data} onChange={onValuesChange}/>
-    <Popover destroyTooltipOnHide
-             overlayClassName={`popover-popover ${overlayClassName || ''}`}
-             placement="right"
-             open={open}
-             onOpenChange={o => setOpen(o)}
-             trigger="click" content={renderForm}>
-      <Button type="link" onClick={() => setOpen(true)} icon={<EditOutlined/>}/>
-    </Popover>
-  </div>)
-}
-
-/**
- * 注册自定义的输入框
- * @param type
- * @param Component
- */
-export const registerInput = (type: string, Component: React.FC<AutoTypeInputProps>) => AdvanceInputConfigs[type] = Component
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {AdvanceInputConfigs, AutoForm, AutoTypeInputProps, FormColumnType} from 'planning-tools'
+import {Button, FormInstance, Popover} from "antd";
+import {EditOutlined} from "@ant-design/icons";
+import React, {useEffect, useState} from "react";
+import {AutoFormFooterProps} from "planning-tools/dist/esm/Components/AutoForm/form";
+import {NgxModuleData} from "../input.ts";
+import './index.less'
+
+export type OnChange = (values: any) => void
+
+export type IContentProps<T = any> = {
+  data: T
+  onChange: OnChange
+}
+
+type IProps = {
+  content: React.FC<IContentProps>
+  columns?: FormColumnType[]
+  renderLines: (values: any) => string[]
+  renderHttpLines?: (values: any) =>string[]
+  overlayClassName?: string
+  labelCol?: number
+}
+export const NgxBasicInput = (
+  {
+    content: ContentComp,
+    columns = [],
+    renderLines,
+      renderHttpLines,
+    overlayClassName,
+    labelCol = 6,
+    value, onChange
+  }: IProps & AutoTypeInputProps) => {
+
+  const [data, setData] = useState<any>({})
+  const [open, setOpen] = useState(false)
+
+  useEffect(() => {
+    if (value?.data) {
+      setData(value.data || {})
+    }
+  }, [value])
+
+  const triggerChange = (values: any) => {
+    const lines = renderLines(values)
+    onChange?.({
+      data: values,
+      lines: lines,
+      http: renderHttpLines?.(values) || []
+    } as NgxModuleData)
+  }
+
+  const onValuesChange = (values: any) => {
+    const current = {...data, ...values}
+    setData(current)
+    setOpen(false)
+    triggerChange(current)
+  }
+
+  const onSubmitData = async (form: FormInstance | null) => {
+    if (!form) {
+      return
+    }
+    const values = await form.validateFields();
+    console.log('values', values);
+    onValuesChange(values)
+  }
+
+  const renderFormFooter = ({formRef}: AutoFormFooterProps) => {
+    return (<div className="form-btns">
+      <Button onClick={() => onSubmitData(formRef.current as never)} type="primary">保存</Button>
+      <Button onClick={() => setOpen(false)}>取消</Button>
+    </div>)
+  }
+
+  const renderForm = () => {
+    return (<AutoForm columns={columns}
+                      formProps={{
+                        labelCol: {span: labelCol}
+                      }}
+                      onlyFields={true}
+                      footer={renderFormFooter}
+                      data={data}/>)
+  }
+
+  return (<div className="popover-input">
+    <ContentComp data={data} onChange={onValuesChange}/>
+    <Popover destroyTooltipOnHide
+             overlayClassName={`popover-popover ${overlayClassName || ''}`}
+             placement="right"
+             open={open}
+             onOpenChange={o => setOpen(o)}
+             trigger="click" content={renderForm}>
+      <Button type="link" onClick={() => setOpen(true)} icon={<EditOutlined/>}/>
+    </Popover>
+  </div>)
+}
+
+/**
+ * 注册自定义的输入框
+ * @param type
+ * @param Component
+ */
+export const registerInput = (type: string, Component: React.FC<AutoTypeInputProps>) => AdvanceInputConfigs[type] = Component

+ 52 - 52
src/pages/nginx/components/certs/index.tsx

@@ -1,52 +1,52 @@
-import {AdvanceInputConfigs, AutoTypeInputProps} from "planning-tools";
-import {Select} from "antd";
-import {useEffect, useState} from "react";
-import {NginxApis} from "../../../../api/nginx.ts";
-import {useAppSelector} from "../../../../store";
-import {INginxCerts} from "../../../../models/api.ts";
-/**
- * @author tuonian
- * @date 2023/7/4
- */
-
-export const CertsSelect = ({value, onChange }: AutoTypeInputProps)=>{
-
-  const [loading,setLoading] = useState(false)
-  const [options,setOptions] = useState<any[]>([])
-
-  const nginx = useAppSelector(state => state.nginx.current)
-
-  const fetchCerts = ()=>{
-    if (!nginx?.id){
-      return
-    }
-    setLoading(true)
-    NginxApis.getCerts(nginx.id)
-      .then(({data})=>{
-        const list = Array.isArray(data.data) ?data.data.map((item: INginxCerts)=>{
-          return {
-            label: item.serviceName,
-            value: item.serviceName
-          }
-        }): []
-        setOptions(list)
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-
-  }
-  useEffect(()=>{
-    fetchCerts()
-  },[])
-
-
-  return (<Select loading={loading}
-                  value={value}
-                  onChange={onChange}
-                  options={options}
-                  style={{marginRight: 10}} />)
-}
-
-
-AdvanceInputConfigs['certs'] = CertsSelect
+import {AdvanceInputConfigs, AutoTypeInputProps} from "planning-tools";
+import {Select} from "antd";
+import {useEffect, useState} from "react";
+import {NginxApis} from "../../../../api/nginx.ts";
+import {useAppSelector} from "../../../../store";
+import {INginxCerts} from "../../../../models/api.ts";
+/**
+ * @author tuonian
+ * @date 2023/7/4
+ */
+
+export const CertsSelect = ({value, onChange }: AutoTypeInputProps)=>{
+
+  const [loading,setLoading] = useState(false)
+  const [options,setOptions] = useState<any[]>([])
+
+  const nginx = useAppSelector(state => state.nginx.current)
+
+  const fetchCerts = ()=>{
+    if (!nginx?.id){
+      return
+    }
+    setLoading(true)
+    NginxApis.getCerts(nginx.id)
+      .then(({data})=>{
+        const list = Array.isArray(data.data) ?data.data.map((item: INginxCerts)=>{
+          return {
+            label: item.serviceName,
+            value: item.serviceName
+          }
+        }): []
+        setOptions(list)
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+
+  }
+  useEffect(()=>{
+    fetchCerts()
+  },[])
+
+
+  return (<Select loading={loading}
+                  value={value}
+                  onChange={onChange}
+                  options={options}
+                  style={{marginRight: 10}} />)
+}
+
+
+AdvanceInputConfigs['certs'] = CertsSelect

+ 53 - 53
src/pages/nginx/components/cors/config.json

@@ -1,53 +1,53 @@
-{
-  "form": [
-    {
-      "title": "域名",
-      "key": "origins",
-      "type": "select",
-      "mode": "tags",
-      "description": "配置允许跨域的域名,多个域名用map指令实现",
-      "option": [],
-      "required": false
-    },
-    {
-      "title": "请求方法",
-      "type": "select",
-      "key": "methods",
-      "required": false,
-      "option": ["GET","POST","PUT","DELETE","OPTIONS"],
-      "description": "配置允许跨域的请求方法",
-      "mode": "tags"
-    },
-    {
-      "title": "请求头",
-      "type": "select",
-      "key": "headers",
-      "required": false,
-      "option": ["Authorization","Content-Type","Accept","Origin","Cache-Control","X-Requested-With"],
-      "description": "配置允许跨域的请求头",
-      "mode": "tags"
-    },
-    {
-      "title": "拦截preflight",
-      "key": "preflight",
-      "required": false,
-      "type": "switch",
-      "description": "拦截preflight的OPTION请求,返回跨域配置"
-    },
-    {
-      "title": "允许发送凭据",
-      "key": "credentials",
-      "type": "switch",
-      "required": false,
-      "description": "可选字段,为true表示允许发送Cookie"
-    },
-    {
-      "title": "跨域缓存",
-      "key": "maxAge",
-      "required": false,
-      "type": "int",
-      "description": "配置 Access-Control-Max-Age,代表着在指定时间(秒)之内不用请求该地址的时候,不需要再进行预检请求,也就是跨域缓存。",
-      "value": 86400
-    }
-  ]
-}
+{
+  "form": [
+    {
+      "title": "域名",
+      "key": "origins",
+      "type": "select",
+      "mode": "tags",
+      "description": "配置允许跨域的域名,多个域名用map指令实现",
+      "option": [],
+      "required": false
+    },
+    {
+      "title": "请求方法",
+      "type": "select",
+      "key": "methods",
+      "required": false,
+      "option": ["GET","POST","PUT","DELETE","OPTIONS"],
+      "description": "配置允许跨域的请求方法",
+      "mode": "tags"
+    },
+    {
+      "title": "请求头",
+      "type": "select",
+      "key": "headers",
+      "required": false,
+      "option": ["Authorization","Content-Type","Accept","Origin","Cache-Control","X-Requested-With"],
+      "description": "配置允许跨域的请求头",
+      "mode": "tags"
+    },
+    {
+      "title": "拦截preflight",
+      "key": "preflight",
+      "required": false,
+      "type": "switch",
+      "description": "拦截preflight的OPTION请求,返回跨域配置"
+    },
+    {
+      "title": "允许发送凭据",
+      "key": "credentials",
+      "type": "switch",
+      "required": false,
+      "description": "可选字段,为true表示允许发送Cookie"
+    },
+    {
+      "title": "跨域缓存",
+      "key": "maxAge",
+      "required": false,
+      "type": "int",
+      "description": "配置 Access-Control-Max-Age,代表着在指定时间(秒)之内不用请求该地址的时候,不需要再进行预检请求,也就是跨域缓存。",
+      "value": 86400
+    }
+  ]
+}

+ 34 - 34
src/pages/nginx/components/cors/index.less

@@ -1,35 +1,35 @@
-.cors-page-overlay{
-  .ant-form-item-row{
-    .ant-form-item-control{
-      .ant-select{
-        width: 100%;
-        max-width: 100%;
-      }
-    }
-  }
-  .form-btns{
-    padding-left: 16%;
-  }
-}
-
-.config-status{
-  &.has-config{
-    color: #1890ff;
-  }
-}
-
-.cors-config-overlay{
-  .ant-tooltip-inner{
-    min-width: 600px;
-    padding: 2px;
-    .ant-input{
-      width: 100%;
-      min-width: 400px;
-      max-width: 100%;
-      color: white;
-      background: #000000;
-      border: none;
-    }
-  }
-
+.cors-page-overlay{
+  .ant-form-item-row{
+    .ant-form-item-control{
+      .ant-select{
+        width: 100%;
+        max-width: 100%;
+      }
+    }
+  }
+  .form-btns{
+    padding-left: 16%;
+  }
+}
+
+.config-status{
+  &.has-config{
+    color: #1890ff;
+  }
+}
+
+.cors-config-overlay{
+  .ant-tooltip-inner{
+    min-width: 600px;
+    padding: 2px;
+    .ant-input{
+      width: 100%;
+      min-width: 400px;
+      max-width: 100%;
+      color: white;
+      background: #000000;
+      border: none;
+    }
+  }
+
 }

+ 110 - 110
src/pages/nginx/components/cors/index.tsx

@@ -1,110 +1,110 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {AutoTypeInputProps, isNull, uniqueKey} from 'planning-tools'
-import './index.less'
-import config from './config.json'
-import {IContentProps, NgxBasicInput, registerInput} from "../basic";
-import {Input, Tooltip} from "antd";
-
-type DataType = {
-  /**
-   * 多域名时的随机key
-   */
-  key?: string
-  origins?: string[]
-  methods?: string[]
-  headers?: string[]
-  preflight?: boolean
-  credentials?: boolean
-  maxAge?: number
-}
-
-export const CorsInput = ({...props}: AutoTypeInputProps)=>{
-
-  const ShowContent = ({data}: IContentProps<DataType>)=>{
-    if (data.origins?.length && data.methods?.length){
-      const lines = renderLines(data);
-      const httpLines = renderHttpLines(data)
-      let hint = ''
-      if (httpLines.length){
-        hint = '# map \n'+ httpLines.join('\n') +'\n'
-      }
-      hint +='# location\n'
-      hint += lines.join('\n')
-
-      return (<Tooltip
-          destroyTooltipOnHide
-          overlayClassName="cors-config-overlay"
-          trigger="click"
-          placement="topLeft"
-          autoAdjustOverflow
-          title={<Input.TextArea disabled rows={Math.min(10,lines.length + httpLines.length + 3)} value={hint} />}>
-        <span className="config-status has-config">已配置</span>
-      </Tooltip>)
-    }
-   return <span className="config-status">未完成配置</span>
-  }
-
-  const renderHttpLines = (values: DataType = {})=>{
-    const lines: string[] = []
-    if (!values.origins?.length){
-      return lines
-    }
-    if (values.origins.length < 2){
-      return lines
-    }
-    lines.push(`map  $http_origin ${values.key}  {`)
-    lines.push(`    default 0;`)
-    values.origins.forEach(host=>{
-      lines.push(`    "~${host}"   ${host};`)
-    })
-    lines.push(`}`)
-    return lines
-  }
-
-  const renderLines = (values: DataType = {})=>{
-    const lines: string[] = []
-    if (!values.key){
-      values.key = `$cors_${uniqueKey(20)}`
-    }
-    if (!values.origins?.length){
-      return lines
-    }
-    if (values.origins.length === 1){
-      lines.push(`add_header 'Access-Control-Allow-Origin' '${values.origins[0]}';`)
-    }else {
-      lines.push(`add_header 'Access-Control-Allow-Origin' ${values.key};`)
-    }
-
-    if (values.methods?.length){
-      lines.push(`add_header 'Access-Control-Allow-Origin' '${values.methods.join(',')}';`)
-    }
-    if (values.headers?.length){
-      lines.push(`add_header 'Access-Control-Allow-Headers' '${values.headers.join(',')}';`)
-    }
-    if (!isNull(values.credentials)){
-      lines.push(`add_header Access-Control-Allow-Credentials   '${values.credentials ? 'true': 'false'}';`)
-    }
-    if (!isNull(values.preflight)){
-      lines.push(`if ($request_method = 'OPTIONS') {
-        return 204;
- }`)
-    }
-    return lines;
-  }
-
-  return <NgxBasicInput
-    {...props}
-    columns={config.form}
-    renderLines={renderLines}
-    renderHttpLines={renderHttpLines}
-    content={ShowContent}
-    overlayClassName="cors-page-overlay"
-    labelCol={4}
-    />
-}
-
-registerInput('cors', CorsInput)
-
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {AutoTypeInputProps, isNull, uniqueKey} from 'planning-tools'
+import './index.less'
+import config from './config.json'
+import {IContentProps, NgxBasicInput, registerInput} from "../basic";
+import {Input, Tooltip} from "antd";
+
+type DataType = {
+  /**
+   * 多域名时的随机key
+   */
+  key?: string
+  origins?: string[]
+  methods?: string[]
+  headers?: string[]
+  preflight?: boolean
+  credentials?: boolean
+  maxAge?: number
+}
+
+export const CorsInput = ({...props}: AutoTypeInputProps)=>{
+
+  const ShowContent = ({data}: IContentProps<DataType>)=>{
+    if (data.origins?.length && data.methods?.length){
+      const lines = renderLines(data);
+      const httpLines = renderHttpLines(data)
+      let hint = ''
+      if (httpLines.length){
+        hint = '# map \n'+ httpLines.join('\n') +'\n'
+      }
+      hint +='# location\n'
+      hint += lines.join('\n')
+
+      return (<Tooltip
+          destroyTooltipOnHide
+          overlayClassName="cors-config-overlay"
+          trigger="click"
+          placement="topLeft"
+          autoAdjustOverflow
+          title={<Input.TextArea disabled rows={Math.min(10,lines.length + httpLines.length + 3)} value={hint} />}>
+        <span className="config-status has-config">已配置</span>
+      </Tooltip>)
+    }
+   return <span className="config-status">未完成配置</span>
+  }
+
+  const renderHttpLines = (values: DataType = {})=>{
+    const lines: string[] = []
+    if (!values.origins?.length){
+      return lines
+    }
+    if (values.origins.length < 2){
+      return lines
+    }
+    lines.push(`map  $http_origin ${values.key}  {`)
+    lines.push(`    default 0;`)
+    values.origins.forEach(host=>{
+      lines.push(`    "~${host}"   ${host};`)
+    })
+    lines.push(`}`)
+    return lines
+  }
+
+  const renderLines = (values: DataType = {})=>{
+    const lines: string[] = []
+    if (!values.key){
+      values.key = `$cors_${uniqueKey(20)}`
+    }
+    if (!values.origins?.length){
+      return lines
+    }
+    if (values.origins.length === 1){
+      lines.push(`add_header 'Access-Control-Allow-Origin' '${values.origins[0]}';`)
+    }else {
+      lines.push(`add_header 'Access-Control-Allow-Origin' ${values.key};`)
+    }
+
+    if (values.methods?.length){
+      lines.push(`add_header 'Access-Control-Allow-Origin' '${values.methods.join(',')}';`)
+    }
+    if (values.headers?.length){
+      lines.push(`add_header 'Access-Control-Allow-Headers' '${values.headers.join(',')}';`)
+    }
+    if (!isNull(values.credentials)){
+      lines.push(`add_header Access-Control-Allow-Credentials   '${values.credentials ? 'true': 'false'}';`)
+    }
+    if (!isNull(values.preflight)){
+      lines.push(`if ($request_method = 'OPTIONS') {
+        return 204;
+ }`)
+    }
+    return lines;
+  }
+
+  return <NgxBasicInput
+    {...props}
+    columns={config.form}
+    renderLines={renderLines}
+    renderHttpLines={renderHttpLines}
+    content={ShowContent}
+    overlayClassName="cors-page-overlay"
+    labelCol={4}
+    />
+}
+
+registerInput('cors', CorsInput)
+

+ 32 - 32
src/pages/nginx/components/error/config.json

@@ -1,32 +1,32 @@
-{
-  "form": [
-    {
-      "title": "错误页面",
-      "type": "array",
-      "key": "error_pages",
-      "required": false,
-      "items": [
-        {
-          "title": "错误码",
-          "key": "codes",
-          "type": "select",
-          "option": ["502","401","403","503"],
-          "mode": "tags"
-        },
-        {
-          "title": "响应码",
-          "key": "respCode",
-          "type": "int",
-          "required": false,
-          "description": "重定向错误页面后,返回给前端的HTTP状态码,比如:404,200,301"
-        },
-        {
-          "title": "跳转uri",
-          "key": "uri",
-          "type": "string",
-          "description": "当错误发生时,跳转的路由或者页面"
-        }
-      ]
-    }
-  ]
-}
+{
+  "form": [
+    {
+      "title": "错误页面",
+      "type": "array",
+      "key": "error_pages",
+      "required": false,
+      "items": [
+        {
+          "title": "错误码",
+          "key": "codes",
+          "type": "select",
+          "option": ["502","401","403","503"],
+          "mode": "tags"
+        },
+        {
+          "title": "响应码",
+          "key": "respCode",
+          "type": "int",
+          "required": false,
+          "description": "重定向错误页面后,返回给前端的HTTP状态码,比如:404,200,301"
+        },
+        {
+          "title": "跳转uri",
+          "key": "uri",
+          "type": "string",
+          "description": "当错误发生时,跳转的路由或者页面"
+        }
+      ]
+    }
+  ]
+}

+ 2 - 2
src/pages/nginx/components/error/index.less

@@ -1,2 +1,2 @@
-.error-page-overlay{
-}
+.error-page-overlay{
+}

+ 73 - 73
src/pages/nginx/components/error/index.tsx

@@ -1,73 +1,73 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {AutoTypeInputProps} from 'planning-tools'
-import './index.less'
-import config from './config.json'
-import {IContentProps, NgxBasicInput, registerInput} from "../basic";
-
-type ErrorPageData = {
-  error_pages?: {
-    /**
-     * 处理的错误状态码
-     */
-    codes: string[]
-    /**
-     * 响应状态码
-     */
-    respCode?: string
-
-    /**
-     * 错误路由,或者命名路由
-     */
-    uri: string
-  }[]
-}
-
-export const ErrorPageInput = ({...props}: AutoTypeInputProps)=>{
-
-  const ShowContent = ({data}: IContentProps<ErrorPageData>)=>{
-    const lines = renderLines(data);
-    return (<div className="error-pages">
-      {
-      lines.map((line,index)=>(<div className="error-page-item" key={index}>{line}</div>))
-      }
-      {
-        lines.length ? null : '未配置'
-      }
-    </div>)
-  }
-
-  const renderLines = (values: ErrorPageData = {})=>{
-    const lines: string[] = []
-    if (!values.error_pages || !values.error_pages.length){
-      return lines
-    }
-    values.error_pages.forEach(item=>{
-      if (!item.codes || item.codes.length ===0 || !item.uri){
-        lines.push(`#error_page code or uri is empty, skip`)
-        return
-      }
-      let text = `error_page  ${item.codes.join(' ')}`
-      if (item.respCode){
-        text += `  =${item.respCode}`
-      }
-      text +=`   ${item.uri};`
-      lines.push(text)
-    })
-    return lines;
-  }
-
-  return <NgxBasicInput
-    {...props}
-    columns={config.form}
-    renderLines={renderLines}
-    content={ShowContent}
-    overlayClassName="error-page-overlay"
-    labelCol={0}
-    />
-}
-
-registerInput('error_page', ErrorPageInput)
-
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {AutoTypeInputProps} from 'planning-tools'
+import './index.less'
+import config from './config.json'
+import {IContentProps, NgxBasicInput, registerInput} from "../basic";
+
+type ErrorPageData = {
+  error_pages?: {
+    /**
+     * 处理的错误状态码
+     */
+    codes: string[]
+    /**
+     * 响应状态码
+     */
+    respCode?: string
+
+    /**
+     * 错误路由,或者命名路由
+     */
+    uri: string
+  }[]
+}
+
+export const ErrorPageInput = ({...props}: AutoTypeInputProps)=>{
+
+  const ShowContent = ({data}: IContentProps<ErrorPageData>)=>{
+    const lines = renderLines(data);
+    return (<div className="error-pages">
+      {
+      lines.map((line,index)=>(<div className="error-page-item" key={index}>{line}</div>))
+      }
+      {
+        lines.length ? null : '未配置'
+      }
+    </div>)
+  }
+
+  const renderLines = (values: ErrorPageData = {})=>{
+    const lines: string[] = []
+    if (!values.error_pages || !values.error_pages.length){
+      return lines
+    }
+    values.error_pages.forEach(item=>{
+      if (!item.codes || item.codes.length ===0 || !item.uri){
+        lines.push(`#error_page code or uri is empty, skip`)
+        return
+      }
+      let text = `error_page  ${item.codes.join(' ')}`
+      if (item.respCode){
+        text += `  =${item.respCode}`
+      }
+      text +=`   ${item.uri};`
+      lines.push(text)
+    })
+    return lines;
+  }
+
+  return <NgxBasicInput
+    {...props}
+    columns={config.form}
+    renderLines={renderLines}
+    content={ShowContent}
+    overlayClassName="error-page-overlay"
+    labelCol={0}
+    />
+}
+
+registerInput('error_page', ErrorPageInput)
+

+ 64 - 64
src/pages/nginx/components/gzip/config.json

@@ -1,64 +1,64 @@
-{
-  "form": [
-    {
-      "title": "gzip_types",
-      "key": "gzip_types",
-      "required": false,
-      "type": "select",
-      "mode": "tags",
-      "option": ["text/html","application/javascript"],
-      "description": "Syntax:\tgzip_types mime-type ...;\nDefault:\t\ngzip_types text/html;\nEnables gzipping of responses for the specified MIME types in addition to “text/html”. The special value “*” matches any MIME type (0.8.29). Responses with the “text/html” type are always compressed."
-    },
-    {
-      "title": "gzip_buffers",
-      "key": "gzip_buffers",
-      "required": false,
-      "type": "string",
-      "description": "Syntax:gzip_buffers number size;\nDefault:gzip_buffers 32 4k|16 8k;\nSets the number and size of buffers used to compress a response. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform."
-    },
-    {
-      "title": "gzip_comp_level",
-      "key": "gzip_comp_level",
-      "required": false,
-      "type": "int",
-      "description": "Syntax:gzip_comp_level level;\nDefault:\ngzip_comp_level 1;\nSets a gzip compression level of a response. Acceptable values are in the range from 1 to 9.\n"
-    },
-    {
-      "title": "gzip_disable",
-      "key": "gzip_disable",
-      "required": false,
-      "type": "string",
-      "description": "Syntax:\tgzip_disable regex ...;\nDisables gzipping of responses for requests with “User-Agent” header fields matching any of the specified regular expressions."
-    },
-    {
-      "title": "gzip_http_version",
-      "key": "gzip_http_version",
-      "required": false,
-      "type": "select",
-      "option": ["1.0","1.1"],
-      "description": "Syntax:\tgzip_http_version 1.0 | 1.1;\nDefault:\t\ngzip_http_version 1.1;"
-    },
-    {
-      "title": "gzip_min_length",
-      "key": "gzip_min_length",
-      "required": false,
-      "type": "int",
-      "description": "Syntax:\tgzip_min_length length;\nDefault:\t\ngzip_min_length 20;\nSets the minimum length of a response that will be gzipped. The length is determined only from the “Content-Length” response header field."
-    },
-    {
-      "title": "gzip_proxied",
-      "key": "gzip_proxied",
-      "required": false,
-      "type": "select",
-      "option": ["off","expired","no-cache","no-store","private","no_last_modified","no_etag","auth","any"],
-      "description": "Syntax:\tgzip_proxied off | expired | no-cache | no-store | private | no_last_modified | no_etag | auth | any ...;\nDefault:\t\ngzip_proxied off;\nEnables or disables gzipping of responses for proxied requests depending on the request and response. The fact that the request is proxied is determined by the presence of the “Via” request header field. The directive accepts multiple parameters:off\ndisables compression for all proxied requests, ignoring other parameters;\nexpired\nenables compression if a response header includes the “Expires” field with a value that disables caching;\nno-cache\nenables compression if a response header includes the “Cache-Control” field with the “no-cache” parameter;\nno-store\nenables compression if a response header includes the “Cache-Control” field with the “no-store” parameter;\nprivate\nenables compression if a response header includes the “Cache-Control” field with the “private” parameter;\nno_last_modified\nenables compression if a response header does not include the “Last-Modified” field;\nno_etag\nenables compression if a response header does not include the “ETag” field;\nauth\nenables compression if a request header includes the “Authorization” field;\nany\nenables compression for all proxied requests."
-    },
-    {
-      "title": "gzip_vary",
-      "key": "gzip_vary",
-      "required": false,
-      "type": "switch",
-      "description": "Syntax:\tgzip_vary on | off;\nDefault:\t\ngzip_vary off;\nEnables or disables inserting the “Vary: Accept-Encoding” response header field if the directives gzip, gzip_static, or gunzip are active."
-    }
-  ]
-}
+{
+  "form": [
+    {
+      "title": "gzip_types",
+      "key": "gzip_types",
+      "required": false,
+      "type": "select",
+      "mode": "tags",
+      "option": ["text/html","application/javascript"],
+      "description": "Syntax:\tgzip_types mime-type ...;\nDefault:\t\ngzip_types text/html;\nEnables gzipping of responses for the specified MIME types in addition to “text/html”. The special value “*” matches any MIME type (0.8.29). Responses with the “text/html” type are always compressed."
+    },
+    {
+      "title": "gzip_buffers",
+      "key": "gzip_buffers",
+      "required": false,
+      "type": "string",
+      "description": "Syntax:gzip_buffers number size;\nDefault:gzip_buffers 32 4k|16 8k;\nSets the number and size of buffers used to compress a response. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform."
+    },
+    {
+      "title": "gzip_comp_level",
+      "key": "gzip_comp_level",
+      "required": false,
+      "type": "int",
+      "description": "Syntax:gzip_comp_level level;\nDefault:\ngzip_comp_level 1;\nSets a gzip compression level of a response. Acceptable values are in the range from 1 to 9.\n"
+    },
+    {
+      "title": "gzip_disable",
+      "key": "gzip_disable",
+      "required": false,
+      "type": "string",
+      "description": "Syntax:\tgzip_disable regex ...;\nDisables gzipping of responses for requests with “User-Agent” header fields matching any of the specified regular expressions."
+    },
+    {
+      "title": "gzip_http_version",
+      "key": "gzip_http_version",
+      "required": false,
+      "type": "select",
+      "option": ["1.0","1.1"],
+      "description": "Syntax:\tgzip_http_version 1.0 | 1.1;\nDefault:\t\ngzip_http_version 1.1;"
+    },
+    {
+      "title": "gzip_min_length",
+      "key": "gzip_min_length",
+      "required": false,
+      "type": "int",
+      "description": "Syntax:\tgzip_min_length length;\nDefault:\t\ngzip_min_length 20;\nSets the minimum length of a response that will be gzipped. The length is determined only from the “Content-Length” response header field."
+    },
+    {
+      "title": "gzip_proxied",
+      "key": "gzip_proxied",
+      "required": false,
+      "type": "select",
+      "option": ["off","expired","no-cache","no-store","private","no_last_modified","no_etag","auth","any"],
+      "description": "Syntax:\tgzip_proxied off | expired | no-cache | no-store | private | no_last_modified | no_etag | auth | any ...;\nDefault:\t\ngzip_proxied off;\nEnables or disables gzipping of responses for proxied requests depending on the request and response. The fact that the request is proxied is determined by the presence of the “Via” request header field. The directive accepts multiple parameters:off\ndisables compression for all proxied requests, ignoring other parameters;\nexpired\nenables compression if a response header includes the “Expires” field with a value that disables caching;\nno-cache\nenables compression if a response header includes the “Cache-Control” field with the “no-cache” parameter;\nno-store\nenables compression if a response header includes the “Cache-Control” field with the “no-store” parameter;\nprivate\nenables compression if a response header includes the “Cache-Control” field with the “private” parameter;\nno_last_modified\nenables compression if a response header does not include the “Last-Modified” field;\nno_etag\nenables compression if a response header does not include the “ETag” field;\nauth\nenables compression if a request header includes the “Authorization” field;\nany\nenables compression for all proxied requests."
+    },
+    {
+      "title": "gzip_vary",
+      "key": "gzip_vary",
+      "required": false,
+      "type": "switch",
+      "description": "Syntax:\tgzip_vary on | off;\nDefault:\t\ngzip_vary off;\nEnables or disables inserting the “Vary: Accept-Encoding” response header field if the directives gzip, gzip_static, or gunzip are active."
+    }
+  ]
+}

+ 10 - 10
src/pages/nginx/components/gzip/index.less

@@ -1,10 +1,10 @@
-.gzip-input{
-  margin-right: 10px;
-}
-
-.gzip-popover{
-  .auto-form{
-    min-width: 450px;
-    width: 550px;
-  }
-}
+.gzip-input{
+  margin-right: 10px;
+}
+
+.gzip-popover{
+  .auto-form{
+    min-width: 450px;
+    width: 550px;
+  }
+}

+ 97 - 97
src/pages/nginx/components/gzip/index.tsx

@@ -1,97 +1,97 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {AdvanceInputConfigs, AutoForm, AutoTypeInputProps, isNull} from 'planning-tools'
-import {Button, FormInstance, Popover, Switch} from "antd";
-import './index.less'
-import {EditOutlined} from "@ant-design/icons";
-import {useEffect, useState} from "react";
-import config from './config.json'
-import {AutoFormFooterProps} from "planning-tools/dist/esm/Components/AutoForm/form";
-import {isBoolean} from "lodash";
-import {NgxModuleData} from "../input.ts";
-
-export const GzipInput = ({value, onChange}:AutoTypeInputProps) => {
-
-  const [data,setData] = useState<any>({})
-  const [open,setOpen] = useState(false)
-
-  useEffect(()=>{
-    if (value?.data){
-      setData(value.data || {})
-    }
-  },[value])
-
-  const onSwitch = (checked: boolean)=>{
-    const values = { ...data,gzip: checked}
-    setData(values)
-    triggerChange(values)
-  }
-
-  const triggerChange = (values: any)=>{
-    const lines:string[] = []
-    if (data?.gzip){
-      lines.push(`gzip      on;`)
-      Object.keys(values).forEach(k=>{
-        let v = values[k];
-        if (isNull(v) || k ==='gzip'){
-          return
-        }
-        if (Array.isArray(v)){
-          v = v.join(' ')
-        }else if (isBoolean(v)){
-          v = v ? 'on' : 'off'
-        }
-        lines.push(`${k}  ${v};`)
-      })
-    }
-    onChange?.({
-      data: values,
-      lines
-    } as NgxModuleData)
-  }
-
-  const onSubmitData =async (form: FormInstance | null)=>{
-    if (!form){
-      return
-    }
-    const values = await form.validateFields();
-    console.log('values', values);
-    const current = { ...data, ...values }
-    setData(current)
-    setOpen(false)
-    triggerChange(current)
-  }
-
-  const renderFormFooter = ({formRef}:AutoFormFooterProps)=>{
-    return (<div style={{textAlign: 'center'}}>
-      <Button onClick={()=>onSubmitData(formRef.current as never)} type="primary">保存</Button>
-      <Button onClick={()=>setOpen(false)}>取消</Button>
-    </div>)
-  }
-
-  const renderForm = ()=>{
-    return (<AutoForm columns={config.form}
-                      formProps={{
-                        labelCol: {span: 6}
-                      }}
-                      onlyFields={true}
-                      footer={renderFormFooter}
-                      data={data} />)
-  }
-
-  return (<div className="gzip-input">
-    <Switch onChange={onSwitch} checked={data.gzip} />
-    <Popover destroyTooltipOnHide overlayClassName="gzip-popover"
-             placement="right"
-             open={open}
-             onOpenChange={o=>setOpen(o)}
-             trigger="click" content={renderForm}>
-      <Button onClick={()=>setOpen(true)} hidden={!data.gzip} type="link" icon={<EditOutlined />} />
-    </Popover>
-
-  </div>)
-}
-
-AdvanceInputConfigs['gzip'] = GzipInput
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {AdvanceInputConfigs, AutoForm, AutoTypeInputProps, isNull} from 'planning-tools'
+import {Button, FormInstance, Popover, Switch} from "antd";
+import './index.less'
+import {EditOutlined} from "@ant-design/icons";
+import {useEffect, useState} from "react";
+import config from './config.json'
+import {AutoFormFooterProps} from "planning-tools/dist/esm/Components/AutoForm/form";
+import {isBoolean} from "lodash";
+import {NgxModuleData} from "../input.ts";
+
+export const GzipInput = ({value, onChange}:AutoTypeInputProps) => {
+
+  const [data,setData] = useState<any>({})
+  const [open,setOpen] = useState(false)
+
+  useEffect(()=>{
+    if (value?.data){
+      setData(value.data || {})
+    }
+  },[value])
+
+  const onSwitch = (checked: boolean)=>{
+    const values = { ...data,gzip: checked}
+    setData(values)
+    triggerChange(values)
+  }
+
+  const triggerChange = (values: any)=>{
+    const lines:string[] = []
+    if (data?.gzip){
+      lines.push(`gzip      on;`)
+      Object.keys(values).forEach(k=>{
+        let v = values[k];
+        if (isNull(v) || k ==='gzip'){
+          return
+        }
+        if (Array.isArray(v)){
+          v = v.join(' ')
+        }else if (isBoolean(v)){
+          v = v ? 'on' : 'off'
+        }
+        lines.push(`${k}  ${v};`)
+      })
+    }
+    onChange?.({
+      data: values,
+      lines
+    } as NgxModuleData)
+  }
+
+  const onSubmitData =async (form: FormInstance | null)=>{
+    if (!form){
+      return
+    }
+    const values = await form.validateFields();
+    console.log('values', values);
+    const current = { ...data, ...values }
+    setData(current)
+    setOpen(false)
+    triggerChange(current)
+  }
+
+  const renderFormFooter = ({formRef}:AutoFormFooterProps)=>{
+    return (<div style={{textAlign: 'center'}}>
+      <Button onClick={()=>onSubmitData(formRef.current as never)} type="primary">保存</Button>
+      <Button onClick={()=>setOpen(false)}>取消</Button>
+    </div>)
+  }
+
+  const renderForm = ()=>{
+    return (<AutoForm columns={config.form}
+                      formProps={{
+                        labelCol: {span: 6}
+                      }}
+                      onlyFields={true}
+                      footer={renderFormFooter}
+                      data={data} />)
+  }
+
+  return (<div className="gzip-input">
+    <Switch onChange={onSwitch} checked={data.gzip} />
+    <Popover destroyTooltipOnHide overlayClassName="gzip-popover"
+             placement="right"
+             open={open}
+             onOpenChange={o=>setOpen(o)}
+             trigger="click" content={renderForm}>
+      <Button onClick={()=>setOpen(true)} hidden={!data.gzip} type="link" icon={<EditOutlined />} />
+    </Popover>
+
+  </div>)
+}
+
+AdvanceInputConfigs['gzip'] = GzipInput

+ 9 - 9
src/pages/nginx/components/index.ts

@@ -1,9 +1,9 @@
-import './proxy/index.tsx'
-import './gzip/index.tsx'
-import './auth'
-import './location'
-import './certs'
-import './proxypass'
-import './proxypass/stream.tsx'
-import './error'
-import './cors'
+import './proxy/index.tsx'
+import './gzip/index.tsx'
+import './auth'
+import './location'
+import './certs'
+import './proxypass'
+import './proxypass/stream.tsx'
+import './error'
+import './cors'

+ 28 - 28
src/pages/nginx/components/input.ts

@@ -1,28 +1,28 @@
-import {isObject} from "planning-tools";
-
-/**
- * 自定义的模块化输入框的数据格式
- */
-export type NgxModuleData<D=any> = {
-  data: D
-  lines?: string[]
-  /**
-   * 渲染到http模块,也就是跟server同级别,没想好怎么搞
-   */
-  http?: string[]
-  /**
-   * 指定为false,则跳过渲染
-   */
-  enable?: boolean
-}
-
-/**
- * 是否是自定义的nginx的输入框
- * @param value
- */
-export const isNgxModuleValue = (value: any)=>{
-  if (!isObject(value)){
-    return false
-  }
-  return !!Array.isArray((value as NgxModuleData)?.lines);
-}
+import {isObject} from "planning-tools";
+
+/**
+ * 自定义的模块化输入框的数据格式
+ */
+export type NgxModuleData<D=any> = {
+  data: D
+  lines?: string[]
+  /**
+   * 渲染到http模块,也就是跟server同级别,没想好怎么搞
+   */
+  http?: string[]
+  /**
+   * 指定为false,则跳过渲染
+   */
+  enable?: boolean
+}
+
+/**
+ * 是否是自定义的nginx的输入框
+ * @param value
+ */
+export const isNgxModuleValue = (value: any)=>{
+  if (!isObject(value)){
+    return false
+  }
+  return !!Array.isArray((value as NgxModuleData)?.lines);
+}

+ 271 - 271
src/pages/nginx/components/location/config.json

@@ -1,272 +1,272 @@
-{
-  "form": [
-    {
-      "key": "name",
-      "type": "string",
-      "title": "名称",
-      "placeholder": "输入名称方便辨别",
-      "required": false
-    },
-    {
-      "key": "match",
-      "type": "object",
-      "title": "匹配路径",
-      "hideHeader": true,
-      "items": [
-        {
-          "type": "select",
-          "title": "匹配规则",
-          "option": [
-            {
-              "label": "精确匹配",
-              "value": "="
-            },
-            {
-              "label": "以字符开头",
-              "value": "^~"
-            },
-            {
-              "label": "正则(区分大小写)",
-              "value": "~"
-            },
-            {
-              "label": "正则(不区分大小写)",
-              "value": "~*"
-            },{
-              "label": "默认",
-              "value": ""
-            }
-          ],
-          "required": false,
-          "key": "regex",
-          "placeholder": "匹配规则,如=,~^",
-          "width": 180
-        },
-        {
-          "type": "string",
-          "title": "路径",
-          "key": "path",
-          "placeholder": "请输入路径",
-          "value": "/",
-          "width": 300
-        }
-      ],
-      "description": "优选级:精确匹配(=) > 完整路径 > 以字符开头(^~) > 正则顺序(~或者~*) > 部分起始路径(/xx/) > 默认路径(/);\n当有正则时,变量$1,$2为正则中匹配的()内的顺序内容,比如:~ \/(d+)\/([0-9]+),当访问/abc/123时,$1为abc,$2为123"
-    },
-    {
-      "key": "enable",
-      "title": "启用",
-      "type": "switch",
-      "description": "是否启用,如果不启用,将不会渲染该配置"
-    },
-    {
-      "key": "proxy_type",
-      "title": "代理类型",
-      "type": "select",
-      "option": [
-        {
-        "label": "反向代理",
-        "value": "proxy"
-      },{
-        "label": "静态资源",
-        "value": "static"
-      },{
-        "label": "其它",
-        "value": "other"
-      }],
-      "cascade": {
-        "proxy": [
-          {
-            "key": "proxy_pass",
-            "title": "代理地址",
-            "type": "proxy_pass"
-          },
-          {
-            "key": "proxy_settings",
-            "title": "更多代理设置",
-            "type": "proxy_settings",
-            "required": false,
-            "description": "更多代理设置"
-          }
-        ],
-        "static": [
-          {
-            "key": "index",
-            "title": "首页",
-            "required": false,
-            "type": "select",
-            "mode": "tags",
-            "option": ["index.html","index.php","index.htm"],
-            "description": "nginx静态资源默认的首页文件名,比如index.html index.php"
-          },
-          {
-            "key": "root",
-            "type": "string",
-            "title": "根路径",
-            "required": false,
-            "description": "静态资源的根路径,查找方式为直接拼接,比如:匹配路径为 /test/ ,root为/data/root,则查找资源的完整路径为:/data/root/test"
-          },
-          {
-            "key": "alias",
-            "type": "string",
-            "title": "路径别名",
-            "required": false,
-            "description": "alias和root二选一,注意与root的区别,比如:匹配路径为 /test/ ,alias为/data/root,则查找资源的完整路径为:/data/root/"
-          },
-          {
-            "key": "try_files",
-            "title": "try_files",
-            "type": "select",
-            "mode": "tags",
-            "option": ["$uri","$uri/","/index.html"],
-            "description": "",
-            "required": false,
-            "width": 450
-          }
-        ],
-        "other": [
-          {
-            "key": "return",
-            "title": "return",
-            "description": "直接返回固定内容",
-            "type": "object",
-            "hideHeader": true,
-            "required": false,
-            "items": [
-              {
-                "key": "code",
-                "type": "int",
-                "min": 200,
-                "max": 600,
-                "placeholder": "请输入http状态码",
-                "width": 180
-              },
-              {
-                "key": "content",
-                "type": "string",
-                "title": "内容",
-                "placeholder": "响应内容",
-                "width": 300
-              }
-            ]
-          }
-        ]
-      }
-    },
-    {
-      "key": "cors_setting",
-      "title": "跨域配置",
-      "type": "cors",
-      "description": "跨域配置,可以通过该配置项解决前端跨域问题",
-      "required": false
-    },
-    {
-      "type": "auth",
-      "title": "鉴权",
-      "key": "auth_request",
-      "required": false,
-      "description": "ngx_http_auth_request_module:实现了基于一子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的"
-    },
-    {
-      "type": "gzip",
-      "title": "压缩配置",
-      "key": "gzip",
-      "required": false,
-      "description": "gzip"
-    },
-    {
-      "key": "add_header",
-      "title": "添加响应头",
-      "type": "array",
-      "hideHeader": true,
-      "description": "添加http响应头",
-      "required": false,
-      "items": [
-        {
-          "key": "name",
-          "title": "header名称",
-          "type": "string",
-          "width": 180,
-          "placeholder": "header名称"
-        },
-        {
-          "key": "value",
-          "title": "header值",
-          "type": "string",
-          "placeholder": "header值",
-          "width": 300
-        }
-      ]
-    },
-    {
-      "key": "rewrite",
-      "type": "object",
-      "title": "rewrite",
-      "required": false,
-      "description": "rewrite:对访问路径进行,放在server{}, if{},location{}段中,rewrite < regex > < replacement > [flag], 必须填写正则表达式和跳转路径才能生效",
-      "items": [
-        {
-          "key": "regex",
-          "title": "正则表达式",
-          "type": "string",
-          "width": 180,
-          "description": "必须填写正则表达式和跳转路径才能生效"
-        },
-        {
-          "key": "replacement",
-          "title": "跳转路径",
-          "type": "string",
-          "placeholder": "跳转后的内容",
-          "width": 300
-        },
-        {
-          "width": 120,
-          "key": "flag",
-          "title": "flag",
-          "type": "select",
-          "option": ["last","break","redirect","permanent"],
-          "description": "last: 相当于Apache的【L】标记,表示完成rewrite;\nbreak:本条规则匹配完成即终止,不在匹配后面的任何规则;\nredirect: 返回302临时重定向,浏览器地址栏会显示跳转后的URL地址,爬虫不会更新url;\npermanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址,爬虫更新url;"
-        }
-      ]
-    },
-    {
-      "key": "tmp_custom_config",
-      "title": "自定义配置",
-      "type": "textarea",
-      "hideHeader": true,
-      "description": "自定义配置,注意,每行结尾需要加“;”号,将会拼接在最后",
-      "required": false,
-      "trim": false
-    },
-    {
-      "key": "internal",
-      "title": "内部路由",
-      "type": "switch",
-      "description": "内部路由:nginx内部访问,一旦出了这个配置文件,则失效"
-    },
-    {
-      "key": "error_page",
-      "title": "错误页面",
-      "type": "error_page",
-      "required": false,
-      "description": "错误页面配置"
-    },
-    {
-      "key": "default_type",
-      "title": "默认内容类型",
-      "type": "string",
-      "required": false,
-      "description": "default_type eg. text/plain application/json"
-    },
-    {
-      "key": "remark",
-      "title": "备注信息",
-      "type": "textarea",
-      "rows": 3,
-      "placeholder": "输入备注信息",
-      "required": false,
-      "trim": false,
-      "width": 600
-    }
-  ]
+{
+  "form": [
+    {
+      "key": "name",
+      "type": "string",
+      "title": "名称",
+      "placeholder": "输入名称方便辨别",
+      "required": false
+    },
+    {
+      "key": "match",
+      "type": "object",
+      "title": "匹配路径",
+      "hideHeader": true,
+      "items": [
+        {
+          "type": "select",
+          "title": "匹配规则",
+          "option": [
+            {
+              "label": "精确匹配",
+              "value": "="
+            },
+            {
+              "label": "以字符开头",
+              "value": "^~"
+            },
+            {
+              "label": "正则(区分大小写)",
+              "value": "~"
+            },
+            {
+              "label": "正则(不区分大小写)",
+              "value": "~*"
+            },{
+              "label": "默认",
+              "value": ""
+            }
+          ],
+          "required": false,
+          "key": "regex",
+          "placeholder": "匹配规则,如=,~^",
+          "width": 180
+        },
+        {
+          "type": "string",
+          "title": "路径",
+          "key": "path",
+          "placeholder": "请输入路径",
+          "value": "/",
+          "width": 300
+        }
+      ],
+      "description": "优选级:精确匹配(=) > 完整路径 > 以字符开头(^~) > 正则顺序(~或者~*) > 部分起始路径(/xx/) > 默认路径(/);\n当有正则时,变量$1,$2为正则中匹配的()内的顺序内容,比如:~ \/(d+)\/([0-9]+),当访问/abc/123时,$1为abc,$2为123"
+    },
+    {
+      "key": "enable",
+      "title": "启用",
+      "type": "switch",
+      "description": "是否启用,如果不启用,将不会渲染该配置"
+    },
+    {
+      "key": "proxy_type",
+      "title": "代理类型",
+      "type": "select",
+      "option": [
+        {
+        "label": "反向代理",
+        "value": "proxy"
+      },{
+        "label": "静态资源",
+        "value": "static"
+      },{
+        "label": "其它",
+        "value": "other"
+      }],
+      "cascade": {
+        "proxy": [
+          {
+            "key": "proxy_pass",
+            "title": "代理地址",
+            "type": "proxy_pass"
+          },
+          {
+            "key": "proxy_settings",
+            "title": "更多代理设置",
+            "type": "proxy_settings",
+            "required": false,
+            "description": "更多代理设置"
+          }
+        ],
+        "static": [
+          {
+            "key": "index",
+            "title": "首页",
+            "required": false,
+            "type": "select",
+            "mode": "tags",
+            "option": ["index.html","index.php","index.htm"],
+            "description": "nginx静态资源默认的首页文件名,比如index.html index.php"
+          },
+          {
+            "key": "root",
+            "type": "string",
+            "title": "根路径",
+            "required": false,
+            "description": "静态资源的根路径,查找方式为直接拼接,比如:匹配路径为 /test/ ,root为/data/root,则查找资源的完整路径为:/data/root/test"
+          },
+          {
+            "key": "alias",
+            "type": "string",
+            "title": "路径别名",
+            "required": false,
+            "description": "alias和root二选一,注意与root的区别,比如:匹配路径为 /test/ ,alias为/data/root,则查找资源的完整路径为:/data/root/"
+          },
+          {
+            "key": "try_files",
+            "title": "try_files",
+            "type": "select",
+            "mode": "tags",
+            "option": ["$uri","$uri/","/index.html"],
+            "description": "",
+            "required": false,
+            "width": 450
+          }
+        ],
+        "other": [
+          {
+            "key": "return",
+            "title": "return",
+            "description": "直接返回固定内容",
+            "type": "object",
+            "hideHeader": true,
+            "required": false,
+            "items": [
+              {
+                "key": "code",
+                "type": "int",
+                "min": 200,
+                "max": 600,
+                "placeholder": "请输入http状态码",
+                "width": 180
+              },
+              {
+                "key": "content",
+                "type": "string",
+                "title": "内容",
+                "placeholder": "响应内容",
+                "width": 300
+              }
+            ]
+          }
+        ]
+      }
+    },
+    {
+      "key": "cors_setting",
+      "title": "跨域配置",
+      "type": "cors",
+      "description": "跨域配置,可以通过该配置项解决前端跨域问题",
+      "required": false
+    },
+    {
+      "type": "auth",
+      "title": "鉴权",
+      "key": "auth_request",
+      "required": false,
+      "description": "ngx_http_auth_request_module:实现了基于一子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的"
+    },
+    {
+      "type": "gzip",
+      "title": "压缩配置",
+      "key": "gzip",
+      "required": false,
+      "description": "gzip"
+    },
+    {
+      "key": "add_header",
+      "title": "添加响应头",
+      "type": "array",
+      "hideHeader": true,
+      "description": "添加http响应头",
+      "required": false,
+      "items": [
+        {
+          "key": "name",
+          "title": "header名称",
+          "type": "string",
+          "width": 180,
+          "placeholder": "header名称"
+        },
+        {
+          "key": "value",
+          "title": "header值",
+          "type": "string",
+          "placeholder": "header值",
+          "width": 300
+        }
+      ]
+    },
+    {
+      "key": "rewrite",
+      "type": "object",
+      "title": "rewrite",
+      "required": false,
+      "description": "rewrite:对访问路径进行,放在server{}, if{},location{}段中,rewrite < regex > < replacement > [flag], 必须填写正则表达式和跳转路径才能生效",
+      "items": [
+        {
+          "key": "regex",
+          "title": "正则表达式",
+          "type": "string",
+          "width": 180,
+          "description": "必须填写正则表达式和跳转路径才能生效"
+        },
+        {
+          "key": "replacement",
+          "title": "跳转路径",
+          "type": "string",
+          "placeholder": "跳转后的内容",
+          "width": 300
+        },
+        {
+          "width": 120,
+          "key": "flag",
+          "title": "flag",
+          "type": "select",
+          "option": ["last","break","redirect","permanent"],
+          "description": "last: 相当于Apache的【L】标记,表示完成rewrite;\nbreak:本条规则匹配完成即终止,不在匹配后面的任何规则;\nredirect: 返回302临时重定向,浏览器地址栏会显示跳转后的URL地址,爬虫不会更新url;\npermanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址,爬虫更新url;"
+        }
+      ]
+    },
+    {
+      "key": "tmp_custom_config",
+      "title": "自定义配置",
+      "type": "textarea",
+      "hideHeader": true,
+      "description": "自定义配置,注意,每行结尾需要加“;”号,将会拼接在最后",
+      "required": false,
+      "trim": false
+    },
+    {
+      "key": "internal",
+      "title": "内部路由",
+      "type": "switch",
+      "description": "内部路由:nginx内部访问,一旦出了这个配置文件,则失效"
+    },
+    {
+      "key": "error_page",
+      "title": "错误页面",
+      "type": "error_page",
+      "required": false,
+      "description": "错误页面配置"
+    },
+    {
+      "key": "default_type",
+      "title": "默认内容类型",
+      "type": "string",
+      "required": false,
+      "description": "default_type eg. text/plain application/json"
+    },
+    {
+      "key": "remark",
+      "title": "备注信息",
+      "type": "textarea",
+      "rows": 3,
+      "placeholder": "输入备注信息",
+      "required": false,
+      "trim": false,
+      "width": 600
+    }
+  ]
 }

+ 41 - 41
src/pages/nginx/components/location/index.less

@@ -1,42 +1,42 @@
-.location-input{
-
-  .ant-drawer-header{
-    padding: 5px 10px;
-  }
-  .ant-drawer-body{
-    padding: 10px;
-  }
-}
-
-.location-table{
-  .ant-table-cell{
-    padding: 10px;
-  }
-  .location-btns{
-    .ant-btn+.ant-btn{
-      margin-left: 5px;
-    }
-    .ant-btn{
-      padding: 2.4px 0;
-    }
-    .ant-btn-link{
-      font-size: 13px;
-    }
-  }
-
-}
-.location-table ~ .description{
-  display: block;
-  width: 100%;
-  box-sizing: border-box;
-  padding-right: 20px;
-}
-
-.location-conf-preview{
-  width: 520px;
-  min-height: 100px;
-  .ant-input[disabled]{
-    color: #333333;
-    background: none;
-  }
+.location-input{
+
+  .ant-drawer-header{
+    padding: 5px 10px;
+  }
+  .ant-drawer-body{
+    padding: 10px;
+  }
+}
+
+.location-table{
+  .ant-table-cell{
+    padding: 10px;
+  }
+  .location-btns{
+    .ant-btn+.ant-btn{
+      margin-left: 5px;
+    }
+    .ant-btn{
+      padding: 2.4px 0;
+    }
+    .ant-btn-link{
+      font-size: 13px;
+    }
+  }
+
+}
+.location-table ~ .description{
+  display: block;
+  width: 100%;
+  box-sizing: border-box;
+  padding-right: 20px;
+}
+
+.location-conf-preview{
+  width: 520px;
+  min-height: 100px;
+  .ant-input[disabled]{
+    color: #333333;
+    background: none;
+  }
 }

+ 280 - 280
src/pages/nginx/components/location/index.tsx

@@ -1,281 +1,281 @@
-import {Button, Drawer, Input, Modal, Popover, Space, Switch, Table} from "antd";
-import {ColumnsType} from "antd/es/table";
-import {
-    AdvanceInputConfigs,
-    AutoForm,
-    AutoFormInstance,
-    AutoTypeInputProps,
-    isNull,
-    Message,
-    uniqueKey
-} from "planning-tools";
-import {useEffect, useRef, useState} from "react";
-import {CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons";
-import {INginxLocation} from "../../../../models/nginx.ts";
-import {cloneDeep} from "lodash";
-import FormConfig from './config.json'
-
-import './index.less'
-import {SiteInput} from "../site";
-import {renderLocation} from "./utils.ts";
-
-/**
- * 部分的重要信息
- * @param data
- * @constructor
- */
-const LocationInfo = ({data}:{ data: INginxLocation})=>{
-
-    const rootDir = ()=>{
-        if (data.alias){
-            return `alias: ${data.alias}`
-        }
-        return `root: ${data.root || '--'}`
-    }
-
-
-    return (<div>
-        {
-            data.proxy_type === 'proxy' ? <div>{`proxy: ${data.proxy_pass}`}</div> : null
-        }
-        {
-            data.proxy_type === 'static' ? <div>{rootDir()}<SiteInput location={data} /></div>:null
-        }
-        <div>
-            {
-                data.rewrite?.regex && data.rewrite?.replacement ? `${data.rewrite.regex} ${data.rewrite.replacement}` : ''
-            }
-        </div>
-    </div>)
-}
-
-/**
- * 路由,站点,规则编辑
- * @param value
- * @param onChange
- * @param column
- * @constructor
- */
-export const LocationInput = ({value, onChange }: AutoTypeInputProps) => {
-
-    const [locations, setLocations] = useState<INginxLocation[]>([])
-
-    const [editData, setEditData] = useState<INginxLocation>()
-    const isAddRef = useRef(false)
-
-    const [modal,contextHolder] = Modal.useModal()
-
-    const formRef = useRef<AutoFormInstance>()
-
-    useEffect(() => {
-        if (Array.isArray(value)) {
-            setLocations(value.map((item: INginxLocation) => {
-                if (!item.id) {
-                    item.id = uniqueKey(20)
-                }
-                if (!item.lines){
-                    renderLocation(item)
-                }
-                return item
-            }))
-        }
-
-    }, [value])
-
-    const onEditRow = (data: INginxLocation) => {
-        isAddRef.current = false
-        setEditData(cloneDeep(data))
-    }
-
-    const onAddData = (data?: INginxLocation, index?: number)=>{
-        isAddRef.current = true
-        setEditData({ ...data,id: uniqueKey(20),__index__: index} as never)
-    }
-
-    const onRemoveData = (data: INginxLocation)=>{
-        const onOk = ()=>{
-            const list = locations.filter(item=>item.id !== data.id);
-            onChange?.(list)
-        }
-        modal.confirm({
-            title: '提示',
-            type: 'warning',
-            content: '您确定要删除该代理/站点吗?删除操作不可恢复,请谨慎操作!',
-            okType: 'danger',
-            okText: '仍要删除',
-            cancelText: '先不了',
-            onOk,
-        })
-    }
-
-    const onQuickChangeStatus = (data: INginxLocation, enable: boolean) => {
-        const list = locations.map(item=>{
-            if (item.id === data.id){
-                return { ...item, enable }
-            }
-            return item
-        })
-        onChange?.(list)
-    }
-
-    const onSubmitData = async () => {
-        const values = await formRef.current?.onSyncSubmit(true);
-        const newData = {...editData, ...values} as INginxLocation;
-        console.log('newLocation', newData);
-        if (!editData) {
-            console.warn('editData is null ,skip ');
-            return
-        }
-        renderLocation(newData)
-        let list: INginxLocation[]
-        if (isAddRef.current){
-            const index = newData.__index__ || locations.length;
-            delete newData.__index__;
-            let exist = locations.find(item=>item.name == newData.name);
-            if (exist){
-                Message.warning('名称不能相同,请修改后再保存!');
-                return
-            }
-            exist = locations.find(item=> {
-                if (item.match && newData.match){
-                    return item.match.regex === newData.match.regex && item.match.path === newData.match.path
-                }
-                return false
-            });
-            if (exist){
-                Message.warning('匹配规则不能完全一样,请修改后重新添加!');
-                return
-            }
-            renderLocation(newData);
-            if (isNull(index) || index < 0 || index >= locations.length-1){
-                list = locations.concat([newData])
-            }else {
-                list = []
-                locations.forEach((item,idx)=>{
-                    if (idx === index){
-                        list.push(item)
-                        list.push(newData)
-                    }else {
-                        list.push(item)
-                    }
-                })
-            }
-        }else {
-            renderLocation(newData);
-            list = locations.map(item => {
-                if (item.id === newData.id) {
-                    return {...item, ...newData}
-                }
-                return item
-            })
-        }
-        onChange?.(list)
-        setEditData(undefined)
-    }
-
-    const renderPreview = (data: INginxLocation)=>{
-        let content ='';
-        let rows = 0;
-        if (data.http?.length){
-           content = data.http.join('\n') + '\n';
-           rows = data.http.length;
-        }
-        if (data.lines){
-            content = content+ data.lines.join('\n')
-            rows +=data.lines.length
-        }
-        return (<div className="location-conf-preview">
-            <Input.TextArea rows={Math.max(Math.min(10,rows),5)} disabled value={content} />
-        </div>)
-    }
-
-    const renderOps = (_: never, data: INginxLocation, index: number) => {
-        return (
-            <div className="location-btns">
-                <Button onClick={() => onRemoveData(data)} type="text" danger icon={<DeleteOutlined/>}/>
-                <Button onClick={() => onEditRow(data)} type="link" icon={<EditOutlined/>}/>
-                <Button onClick={()=>onAddData(data, index)} type="link" icon={<CopyOutlined/>}/>
-                <Popover trigger="click" destroyTooltipOnHide
-                         placement="top"
-                         content={()=>renderPreview(data as never)} >
-                    <Button type="link">预览</Button>
-                </Popover>
-            </div>
-        )
-    }
-
-    const columns: ColumnsType = [
-        {
-            dataIndex: 'name',
-            title: '路由名称',
-            width: 120
-        },
-        {
-            dataIndex: 'match',
-            title: "规则",
-            render: (value) => <span>{`${value.regex || ''} ${value.path}`}</span>
-        },
-        {
-            dataIndex: 'enable',
-            title: '状态',
-            render: (value,record) => <Switch onChange={c=>onQuickChangeStatus(record as never,c)} checked={value}/>
-        },
-        {
-            dataIndex: 'proxy_pass',
-            title: '代理或路径',
-            render: (_,record: any)=>{
-                return (<LocationInfo data={record} />)
-            }
-        },
-        {
-          dataIndex: 'remark',
-          title:"备注",
-        },
-        {
-            title: '操作',
-            render: renderOps as never,
-            width: 180,
-            fixed: 'right'
-        }
-    ]
-
-    return (
-        <>
-
-            {
-                locations.length ? (<Table pagination={false}
-                                           style={{marginRight: 5}}
-                                           rowKey="id"
-                                           columns={columns as never}
-                                           className="location-table"
-                                           dataSource={locations}>
-                    <div>Empty</div>
-                </Table>) : (
-                    <>
-                        <Button onClick={()=>onAddData()} className="add-btn" type="link" icon={<PlusOutlined/>}/>
-                    </>
-                )
-            }
-            <Drawer title={isAddRef.current? '新增' : '编辑'}
-                    placement="right"
-                    open={!!editData}
-                    onClose={() => setEditData(undefined)}
-                    destroyOnClose
-                    width={900}
-                    className="location-input"
-                    extra={<Space>
-                        <Button onClick={onSubmitData} ghost type="primary">保存</Button>
-                    </Space>}
-            >
-                <AutoForm
-                    columns={FormConfig.form}
-                    ref={formRef as never}
-                    data={editData}/>
-            </Drawer>
-            {contextHolder}
-        </>
-    )
-}
-
-
+import {Button, Drawer, Input, Modal, Popover, Space, Switch, Table} from "antd";
+import {ColumnsType} from "antd/es/table";
+import {
+    AdvanceInputConfigs,
+    AutoForm,
+    AutoFormInstance,
+    AutoTypeInputProps,
+    isNull,
+    Message,
+    uniqueKey
+} from "planning-tools";
+import {useEffect, useRef, useState} from "react";
+import {CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons";
+import {INginxLocation} from "../../../../models/nginx.ts";
+import {cloneDeep} from "lodash";
+import FormConfig from './config.json'
+
+import './index.less'
+import {SiteInput} from "../site";
+import {renderLocation} from "./utils.ts";
+
+/**
+ * 部分的重要信息
+ * @param data
+ * @constructor
+ */
+const LocationInfo = ({data}:{ data: INginxLocation})=>{
+
+    const rootDir = ()=>{
+        if (data.alias){
+            return `alias: ${data.alias}`
+        }
+        return `root: ${data.root || '--'}`
+    }
+
+
+    return (<div>
+        {
+            data.proxy_type === 'proxy' ? <div>{`proxy: ${data.proxy_pass}`}</div> : null
+        }
+        {
+            data.proxy_type === 'static' ? <div>{rootDir()}<SiteInput location={data} /></div>:null
+        }
+        <div>
+            {
+                data.rewrite?.regex && data.rewrite?.replacement ? `${data.rewrite.regex} ${data.rewrite.replacement}` : ''
+            }
+        </div>
+    </div>)
+}
+
+/**
+ * 路由,站点,规则编辑
+ * @param value
+ * @param onChange
+ * @param column
+ * @constructor
+ */
+export const LocationInput = ({value, onChange }: AutoTypeInputProps) => {
+
+    const [locations, setLocations] = useState<INginxLocation[]>([])
+
+    const [editData, setEditData] = useState<INginxLocation>()
+    const isAddRef = useRef(false)
+
+    const [modal,contextHolder] = Modal.useModal()
+
+    const formRef = useRef<AutoFormInstance>()
+
+    useEffect(() => {
+        if (Array.isArray(value)) {
+            setLocations(value.map((item: INginxLocation) => {
+                if (!item.id) {
+                    item.id = uniqueKey(20)
+                }
+                if (!item.lines){
+                    renderLocation(item)
+                }
+                return item
+            }))
+        }
+
+    }, [value])
+
+    const onEditRow = (data: INginxLocation) => {
+        isAddRef.current = false
+        setEditData(cloneDeep(data))
+    }
+
+    const onAddData = (data?: INginxLocation, index?: number)=>{
+        isAddRef.current = true
+        setEditData({ ...data,id: uniqueKey(20),__index__: index} as never)
+    }
+
+    const onRemoveData = (data: INginxLocation)=>{
+        const onOk = ()=>{
+            const list = locations.filter(item=>item.id !== data.id);
+            onChange?.(list)
+        }
+        modal.confirm({
+            title: '提示',
+            type: 'warning',
+            content: '您确定要删除该代理/站点吗?删除操作不可恢复,请谨慎操作!',
+            okType: 'danger',
+            okText: '仍要删除',
+            cancelText: '先不了',
+            onOk,
+        })
+    }
+
+    const onQuickChangeStatus = (data: INginxLocation, enable: boolean) => {
+        const list = locations.map(item=>{
+            if (item.id === data.id){
+                return { ...item, enable }
+            }
+            return item
+        })
+        onChange?.(list)
+    }
+
+    const onSubmitData = async () => {
+        const values = await formRef.current?.onSyncSubmit(true);
+        const newData = {...editData, ...values} as INginxLocation;
+        console.log('newLocation', newData);
+        if (!editData) {
+            console.warn('editData is null ,skip ');
+            return
+        }
+        renderLocation(newData)
+        let list: INginxLocation[]
+        if (isAddRef.current){
+            const index = newData.__index__ || locations.length;
+            delete newData.__index__;
+            let exist = locations.find(item=>item.name == newData.name);
+            if (exist){
+                Message.warning('名称不能相同,请修改后再保存!');
+                return
+            }
+            exist = locations.find(item=> {
+                if (item.match && newData.match){
+                    return item.match.regex === newData.match.regex && item.match.path === newData.match.path
+                }
+                return false
+            });
+            if (exist){
+                Message.warning('匹配规则不能完全一样,请修改后重新添加!');
+                return
+            }
+            renderLocation(newData);
+            if (isNull(index) || index < 0 || index >= locations.length-1){
+                list = locations.concat([newData])
+            }else {
+                list = []
+                locations.forEach((item,idx)=>{
+                    if (idx === index){
+                        list.push(item)
+                        list.push(newData)
+                    }else {
+                        list.push(item)
+                    }
+                })
+            }
+        }else {
+            renderLocation(newData);
+            list = locations.map(item => {
+                if (item.id === newData.id) {
+                    return {...item, ...newData}
+                }
+                return item
+            })
+        }
+        onChange?.(list)
+        setEditData(undefined)
+    }
+
+    const renderPreview = (data: INginxLocation)=>{
+        let content ='';
+        let rows = 0;
+        if (data.http?.length){
+           content = data.http.join('\n') + '\n';
+           rows = data.http.length;
+        }
+        if (data.lines){
+            content = content+ data.lines.join('\n')
+            rows +=data.lines.length
+        }
+        return (<div className="location-conf-preview">
+            <Input.TextArea rows={Math.max(Math.min(10,rows),5)} disabled value={content} />
+        </div>)
+    }
+
+    const renderOps = (_: never, data: INginxLocation, index: number) => {
+        return (
+            <div className="location-btns">
+                <Button onClick={() => onRemoveData(data)} type="text" danger icon={<DeleteOutlined/>}/>
+                <Button onClick={() => onEditRow(data)} type="link" icon={<EditOutlined/>}/>
+                <Button onClick={()=>onAddData(data, index)} type="link" icon={<CopyOutlined/>}/>
+                <Popover trigger="click" destroyTooltipOnHide
+                         placement="top"
+                         content={()=>renderPreview(data as never)} >
+                    <Button type="link">预览</Button>
+                </Popover>
+            </div>
+        )
+    }
+
+    const columns: ColumnsType = [
+        {
+            dataIndex: 'name',
+            title: '路由名称',
+            width: 120
+        },
+        {
+            dataIndex: 'match',
+            title: "规则",
+            render: (value) => <span>{`${value.regex || ''} ${value.path}`}</span>
+        },
+        {
+            dataIndex: 'enable',
+            title: '状态',
+            render: (value,record) => <Switch onChange={c=>onQuickChangeStatus(record as never,c)} checked={value}/>
+        },
+        {
+            dataIndex: 'proxy_pass',
+            title: '代理或路径',
+            render: (_,record: any)=>{
+                return (<LocationInfo data={record} />)
+            }
+        },
+        {
+          dataIndex: 'remark',
+          title:"备注",
+        },
+        {
+            title: '操作',
+            render: renderOps as never,
+            width: 180,
+            fixed: 'right'
+        }
+    ]
+
+    return (
+        <>
+
+            {
+                locations.length ? (<Table pagination={false}
+                                           style={{marginRight: 5}}
+                                           rowKey="id"
+                                           columns={columns as never}
+                                           className="location-table"
+                                           dataSource={locations}>
+                    <div>Empty</div>
+                </Table>) : (
+                    <>
+                        <Button onClick={()=>onAddData()} className="add-btn" type="link" icon={<PlusOutlined/>}/>
+                    </>
+                )
+            }
+            <Drawer title={isAddRef.current? '新增' : '编辑'}
+                    placement="right"
+                    open={!!editData}
+                    onClose={() => setEditData(undefined)}
+                    destroyOnClose
+                    width={900}
+                    className="location-input"
+                    extra={<Space>
+                        <Button onClick={onSubmitData} ghost type="primary">保存</Button>
+                    </Space>}
+            >
+                <AutoForm
+                    columns={FormConfig.form}
+                    ref={formRef as never}
+                    data={editData}/>
+            </Drawer>
+            {contextHolder}
+        </>
+    )
+}
+
+
 AdvanceInputConfigs['locations'] = LocationInput

+ 107 - 107
src/pages/nginx/components/location/utils.ts

@@ -1,108 +1,108 @@
-import {INginxLocation} from "../../../../models/nginx.ts";
-import {cloneDeep} from "lodash";
-import {isNgxModuleValue, NgxModuleData} from "../input.ts";
-import {isBasicData} from "planning-tools";
-
-/**
- * 临时数据,不渲染
- */
-const blacklist: { [key:string]:boolean } = {
-    'rewrite': true,
-    'add_header': true,
-    'id': true,
-    'name': true,
-    'proxy_type': true,
-    'enable': true,
-    "remark": true,
-    "nginxId": true,
-    "proxy_set_header": true,
-    "__index__": true,
-    "internal": true,
-    "lines": true,
-    http:true,
-    data: true
-}
-
-/**
- * 这里暂时要考虑下
- * @param origin
- * @param httpLines
- */
-export const renderLocation = (origin: INginxLocation) => {
-    const loc = cloneDeep(origin)
-    const lines: string[] = [];
-    const httpLines: string[] = [];
-    origin.lines = lines;
-    origin.http = httpLines;
-
-    lines.push('')
-    lines.push(`####   ${loc.name || loc.id} start...`)
-    lines.push(`location ${loc.match.regex || ''} ${loc.match.path || '/'} {`)
-
-    if (loc.rewrite && loc.rewrite.replacement && loc.rewrite.regex){
-        lines.push(`    rewrite ${loc.rewrite.regex} ${loc.rewrite.replacement} ${loc.rewrite.flag || ''};`)
-    }
-
-    (loc.add_header || []).forEach(h=>{
-        lines.push(`    add_header ${h.name} ${h.value};`)
-    })
-
-    if (loc.proxy_type !=='proxy'){
-        delete loc.proxy_settings
-        delete loc.proxy_pass
-        delete loc.proxy_set_header
-        console.log('loc', loc)
-    }
-
-    if (loc.proxy_type !== 'static'){
-        delete loc.root
-        delete loc.alias
-    }
-    if (loc.proxy_type !== 'other'){
-        delete loc.return
-    }
-
-    if (loc.internal){
-        lines.push('    internal;');
-    }
-    delete loc.internal;
-
-    Object.keys(loc).forEach(k=>{
-        if (blacklist[k]){
-            return;
-        }
-        if (k.startsWith("tmp" || k.startsWith("temp"))){
-            return;
-        }
-        let value = (loc as any)[k];
-        if (Array.isArray(value)){
-            value = value.join(' ')
-        }else if (isNgxModuleValue(value)){
-            value.lines.forEach((line: string)=>{
-                lines.push(`    ${line}`)
-            });
-            (value as NgxModuleData).http?.forEach(l=>httpLines.push(l))
-            value = '';
-        } else if (!isBasicData(value)){
-            console.log('[render] skip',k, value)
-            value = ''
-        }
-        if (value){
-            lines.push(`    ${k}      ${value};`)
-        }
-    })
-
-    if (loc.tmp_custom_config){
-        loc.tmp_custom_config.split('\n').forEach(line=>{
-            lines.push(`    ${line}`)
-        })
-    }
-    if (loc.return && loc.return.code){
-        lines.push(`    return  ${loc.return.code}  ${loc.return.content};`)
-    }
-
-    lines.push('}')
-    lines.push(`####   ${loc.name || loc.id} end...`)
-    lines.push('')
-    return lines
+import {INginxLocation} from "../../../../models/nginx.ts";
+import {cloneDeep} from "lodash";
+import {isNgxModuleValue, NgxModuleData} from "../input.ts";
+import {isBasicData} from "planning-tools";
+
+/**
+ * 临时数据,不渲染
+ */
+const blacklist: { [key:string]:boolean } = {
+    'rewrite': true,
+    'add_header': true,
+    'id': true,
+    'name': true,
+    'proxy_type': true,
+    'enable': true,
+    "remark": true,
+    "nginxId": true,
+    "proxy_set_header": true,
+    "__index__": true,
+    "internal": true,
+    "lines": true,
+    http:true,
+    data: true
+}
+
+/**
+ * 这里暂时要考虑下
+ * @param origin
+ * @param httpLines
+ */
+export const renderLocation = (origin: INginxLocation) => {
+    const loc = cloneDeep(origin)
+    const lines: string[] = [];
+    const httpLines: string[] = [];
+    origin.lines = lines;
+    origin.http = httpLines;
+
+    lines.push('')
+    lines.push(`####   ${loc.name || loc.id} start...`)
+    lines.push(`location ${loc.match.regex || ''} ${loc.match.path || '/'} {`)
+
+    if (loc.rewrite && loc.rewrite.replacement && loc.rewrite.regex){
+        lines.push(`    rewrite ${loc.rewrite.regex} ${loc.rewrite.replacement} ${loc.rewrite.flag || ''};`)
+    }
+
+    (loc.add_header || []).forEach(h=>{
+        lines.push(`    add_header ${h.name} ${h.value};`)
+    })
+
+    if (loc.proxy_type !=='proxy'){
+        delete loc.proxy_settings
+        delete loc.proxy_pass
+        delete loc.proxy_set_header
+        console.log('loc', loc)
+    }
+
+    if (loc.proxy_type !== 'static'){
+        delete loc.root
+        delete loc.alias
+    }
+    if (loc.proxy_type !== 'other'){
+        delete loc.return
+    }
+
+    if (loc.internal){
+        lines.push('    internal;');
+    }
+    delete loc.internal;
+
+    Object.keys(loc).forEach(k=>{
+        if (blacklist[k]){
+            return;
+        }
+        if (k.startsWith("tmp" || k.startsWith("temp"))){
+            return;
+        }
+        let value = (loc as any)[k];
+        if (Array.isArray(value)){
+            value = value.join(' ')
+        }else if (isNgxModuleValue(value)){
+            value.lines.forEach((line: string)=>{
+                lines.push(`    ${line}`)
+            });
+            (value as NgxModuleData).http?.forEach(l=>httpLines.push(l))
+            value = '';
+        } else if (!isBasicData(value)){
+            console.log('[render] skip',k, value)
+            value = ''
+        }
+        if (value){
+            lines.push(`    ${k}      ${value};`)
+        }
+    })
+
+    if (loc.tmp_custom_config){
+        loc.tmp_custom_config.split('\n').forEach(line=>{
+            lines.push(`    ${line}`)
+        })
+    }
+    if (loc.return && loc.return.code){
+        lines.push(`    return  ${loc.return.code}  ${loc.return.content};`)
+    }
+
+    lines.push('}')
+    lines.push(`####   ${loc.name || loc.id} end...`)
+    lines.push('')
+    return lines
 }

+ 195 - 195
src/pages/nginx/components/proxy/config.json

@@ -1,195 +1,195 @@
-{
-  "form": [
-    {
-      "key": "tmp_trans_ip",
-      "title": "透传客户端IP",
-      "type": "switch",
-      "required": false,
-      "description": "添加:proxy_set_header X-Real_IP $remote_addr,X-Forwarded-For $proxy_add_x_forwarded_for"
-    },
-    {
-      "key": "tmp_trans_host",
-      "title": "改写访问域名",
-      "type": "switch",
-      "required": false,
-      "description": "添加:proxy_set_header Host $host"
-    },
-    {
-      "key": "tmp_support_ws",
-      "title": "支持Websocket",
-      "type": "switch",
-      "required": false,
-      "description": "添加websocket代理需要的请求头"
-    },
-    {
-      "key": "proxy_connect_timeout",
-      "title": "连接超时时间",
-      "type": "string",
-      "required": false,
-      "ruleType": "reg",
-      "pattern": "^(\\d+(s|m|h))?$",
-      "description": "eg. 60s 5m 1h"
-    },
-    {
-      "key": "proxy_read_timeout",
-      "title": "读超时时间",
-      "type": "string",
-      "ruleType": "reg",
-      "pattern": "[\\d+](s|m|h)$",
-      "required": false,
-      "description": "proxy_read_timeout"
-    },
-    {
-      "key": "proxy_http_version",
-      "title": "代理http版本",
-      "type": "select",
-      "option": ["1.0","1.1"],
-      "required": false
-    },
-    {
-      "key": "proxy_set_header",
-      "title": "代理请求头",
-      "type": "array",
-      "items": [
-        {
-          "key": "name",
-          "type": "string",
-          "placeholder": "请求头名称",
-          "title": "请求头名称",
-          "mode": "tags",
-          "option": ["Host","X-Real-IP","X-Forwarded-For","Upgrade","Connection"],
-          "description": "Host,X-Real-IP,X-Forwarded-For,Upgrade,Connection"
-        },
-        {
-          "key": "value",
-          "type": "string",
-          "mode": "tags",
-          "placeholder": "请求头值",
-          "title": "请求头值",
-          "option": ["$host","$remote_addr","$proxy_add_x_forwarded_for","$http_upgrade","upgrade"],
-          "description": "如:$host,$remote_addr,$proxy_add_x_forwarded_for,$http_upgrade,upgrade"
-        }
-      ],
-      "required": false
-    },
-    {
-      "key": "proxy_next_upstream",
-      "title": "proxy_next_upstream",
-      "type": "select",
-      "option": ["error","timeout","invalid_header","http_500","http_502","http_503","http_504","http_403","http_404","http_429","non_idempotent","off"],
-      "mode": "multiple",
-      "description": "default: proxy_next_upstream error timeout",
-      "required": false,
-      "width": 425
-    },
-    {
-      "key": "proxy_next_upstream_timeout",
-      "title": "next_upstream_timeout",
-      "type": "string",
-      "required": false,
-      "description": "eg. 60s 5m"
-    },
-    {
-      "key": "proxy_next_upstream_tries",
-      "title": "proxy_next_upstream_tries",
-      "type": "int",
-      "required": false,
-      "description": "proxy_next_upstream_tries,default is 0"
-    },
-    {
-      "key": "proxy_custom_config",
-      "title": "自定义配置",
-      "type": "textarea",
-      "hideHeader": true,
-      "description": "参考文档: https://nginx.org/en/docs/http/ngx_http_proxy_module.html",
-      "required": false,
-      "placeholder": "将会拼接到http的配置文件后,请注意格式",
-      "width": 425
-    },
-    {
-      "key": "tmp_proxy_more",
-      "title": "更多设置",
-      "collapsible": true,
-      "type": "divider",
-      "items": [
-        {
-          "key": "proxy_send_timeout",
-          "title": "proxy_send_timeout",
-          "type": "string",
-          "ruleType": "reg",
-          "pattern": "[\\d+](s|m|h)$",
-          "required": false
-        },
-        {
-          "key": "proxy_redirect",
-          "title": "重定向(proxy_redirect)",
-          "type": "string",
-          "required": false,
-          "description": "请输入 default 或者off 或者 redirect replacement,eg. http://upstream:port/two/ /one/"
-        },
-        {
-          "key": "proxy_pass_request_body",
-          "title": "发送请求数据",
-          "description": "proxy_pass_request_body: 特定场景,不需要将数据转发到服务端,默认发送;关闭会同时添加 proxy_set_header Content-Length \"\" ",
-          "type": "switch",
-          "value": true,
-          "required": false
-        },
-        {
-          "key": "ssl_certificate",
-          "title": "SSL证书",
-          "type": "certs",
-          "required": false
-        },
-        {
-          "key": "proxy_ssl_ciphers",
-          "title": "proxy_ssl_ciphers",
-          "type": "string",
-          "required": false
-        },
-        {
-          "key": "proxy_ssl_protocols",
-          "title": "proxy_ssl_protocols",
-          "type": "select",
-          "option": ["SSLv2","SSLv3","SSLv1","SSLv1.1","SSLv1.2","SSLv1.3"],
-          "mode": "multiple",
-          "required": false
-        },
-        {
-          "key": "proxy_ssl_verify",
-          "title": "证书校验",
-          "type": "switch",
-          "required": false
-        },
-        {
-          "key": "proxy_store",
-          "title": "proxy_store",
-          "type": "string",
-          "required": false,
-          "description": "Enables saving of files to a disk. The on parameter saves files with paths corresponding to the directives alias or root. The off parameter disables saving of files. In addition, the file name can be set explicitly using the string with variables:\n\nproxy_store /data/www$original_uri;"
-        },
-        {
-          "key": "proxy_store_access",
-          "title": "proxy_store_access",
-          "type": "string",
-          "required": false,
-          "description": "Sets access permissions for newly created files and directories, e.g.:\n\nproxy_store_access user:rw group:rw all:r;\nIf any group or all access permissions are specified then user permissions may be omitted:\n\nproxy_store_access group:rw all:r;"
-        },
-        {
-          "key": "proxy_temp_file_write_size",
-          "title": "proxy_temp_file_write_size",
-          "type": "string",
-          "required": false,
-          "description": "Default:\t\nproxy_temp_file_write_size 8k|16k;"
-        },
-        {
-          "key": "proxy_temp_path",
-          "title": "proxy_temp_path",
-          "type": "string",
-          "required": false,
-          "description": "Syntax: proxy_temp_path path [level1 [level2 [level3]]];eg. proxy_temp_path /spool/nginx/proxy_temp 1 2;"
-        }
-      ]
-    }
-  ]
-}
+{
+  "form": [
+    {
+      "key": "tmp_trans_ip",
+      "title": "透传客户端IP",
+      "type": "switch",
+      "required": false,
+      "description": "添加:proxy_set_header X-Real_IP $remote_addr,X-Forwarded-For $proxy_add_x_forwarded_for"
+    },
+    {
+      "key": "tmp_trans_host",
+      "title": "改写访问域名",
+      "type": "switch",
+      "required": false,
+      "description": "添加:proxy_set_header Host $host"
+    },
+    {
+      "key": "tmp_support_ws",
+      "title": "支持Websocket",
+      "type": "switch",
+      "required": false,
+      "description": "添加websocket代理需要的请求头"
+    },
+    {
+      "key": "proxy_connect_timeout",
+      "title": "连接超时时间",
+      "type": "string",
+      "required": false,
+      "ruleType": "reg",
+      "pattern": "^(\\d+(s|m|h))?$",
+      "description": "eg. 60s 5m 1h"
+    },
+    {
+      "key": "proxy_read_timeout",
+      "title": "读超时时间",
+      "type": "string",
+      "ruleType": "reg",
+      "pattern": "[\\d+](s|m|h)$",
+      "required": false,
+      "description": "proxy_read_timeout"
+    },
+    {
+      "key": "proxy_http_version",
+      "title": "代理http版本",
+      "type": "select",
+      "option": ["1.0","1.1"],
+      "required": false
+    },
+    {
+      "key": "proxy_set_header",
+      "title": "代理请求头",
+      "type": "array",
+      "items": [
+        {
+          "key": "name",
+          "type": "string",
+          "placeholder": "请求头名称",
+          "title": "请求头名称",
+          "mode": "tags",
+          "option": ["Host","X-Real-IP","X-Forwarded-For","Upgrade","Connection"],
+          "description": "Host,X-Real-IP,X-Forwarded-For,Upgrade,Connection"
+        },
+        {
+          "key": "value",
+          "type": "string",
+          "mode": "tags",
+          "placeholder": "请求头值",
+          "title": "请求头值",
+          "option": ["$host","$remote_addr","$proxy_add_x_forwarded_for","$http_upgrade","upgrade"],
+          "description": "如:$host,$remote_addr,$proxy_add_x_forwarded_for,$http_upgrade,upgrade"
+        }
+      ],
+      "required": false
+    },
+    {
+      "key": "proxy_next_upstream",
+      "title": "proxy_next_upstream",
+      "type": "select",
+      "option": ["error","timeout","invalid_header","http_500","http_502","http_503","http_504","http_403","http_404","http_429","non_idempotent","off"],
+      "mode": "multiple",
+      "description": "default: proxy_next_upstream error timeout",
+      "required": false,
+      "width": 425
+    },
+    {
+      "key": "proxy_next_upstream_timeout",
+      "title": "next_upstream_timeout",
+      "type": "string",
+      "required": false,
+      "description": "eg. 60s 5m"
+    },
+    {
+      "key": "proxy_next_upstream_tries",
+      "title": "proxy_next_upstream_tries",
+      "type": "int",
+      "required": false,
+      "description": "proxy_next_upstream_tries,default is 0"
+    },
+    {
+      "key": "proxy_custom_config",
+      "title": "自定义配置",
+      "type": "textarea",
+      "hideHeader": true,
+      "description": "参考文档: https://nginx.org/en/docs/http/ngx_http_proxy_module.html",
+      "required": false,
+      "placeholder": "将会拼接到http的配置文件后,请注意格式",
+      "width": 425
+    },
+    {
+      "key": "tmp_proxy_more",
+      "title": "更多设置",
+      "collapsible": true,
+      "type": "divider",
+      "items": [
+        {
+          "key": "proxy_send_timeout",
+          "title": "proxy_send_timeout",
+          "type": "string",
+          "ruleType": "reg",
+          "pattern": "[\\d+](s|m|h)$",
+          "required": false
+        },
+        {
+          "key": "proxy_redirect",
+          "title": "重定向(proxy_redirect)",
+          "type": "string",
+          "required": false,
+          "description": "请输入 default 或者off 或者 redirect replacement,eg. http://upstream:port/two/ /one/"
+        },
+        {
+          "key": "proxy_pass_request_body",
+          "title": "发送请求数据",
+          "description": "proxy_pass_request_body: 特定场景,不需要将数据转发到服务端,默认发送;关闭会同时添加 proxy_set_header Content-Length \"\" ",
+          "type": "switch",
+          "value": true,
+          "required": false
+        },
+        {
+          "key": "ssl_certificate",
+          "title": "SSL证书",
+          "type": "certs",
+          "required": false
+        },
+        {
+          "key": "proxy_ssl_ciphers",
+          "title": "proxy_ssl_ciphers",
+          "type": "string",
+          "required": false
+        },
+        {
+          "key": "proxy_ssl_protocols",
+          "title": "proxy_ssl_protocols",
+          "type": "select",
+          "option": ["SSLv2","SSLv3","SSLv1","SSLv1.1","SSLv1.2","SSLv1.3"],
+          "mode": "multiple",
+          "required": false
+        },
+        {
+          "key": "proxy_ssl_verify",
+          "title": "证书校验",
+          "type": "switch",
+          "required": false
+        },
+        {
+          "key": "proxy_store",
+          "title": "proxy_store",
+          "type": "string",
+          "required": false,
+          "description": "Enables saving of files to a disk. The on parameter saves files with paths corresponding to the directives alias or root. The off parameter disables saving of files. In addition, the file name can be set explicitly using the string with variables:\n\nproxy_store /data/www$original_uri;"
+        },
+        {
+          "key": "proxy_store_access",
+          "title": "proxy_store_access",
+          "type": "string",
+          "required": false,
+          "description": "Sets access permissions for newly created files and directories, e.g.:\n\nproxy_store_access user:rw group:rw all:r;\nIf any group or all access permissions are specified then user permissions may be omitted:\n\nproxy_store_access group:rw all:r;"
+        },
+        {
+          "key": "proxy_temp_file_write_size",
+          "title": "proxy_temp_file_write_size",
+          "type": "string",
+          "required": false,
+          "description": "Default:\t\nproxy_temp_file_write_size 8k|16k;"
+        },
+        {
+          "key": "proxy_temp_path",
+          "title": "proxy_temp_path",
+          "type": "string",
+          "required": false,
+          "description": "Syntax: proxy_temp_path path [level1 [level2 [level3]]];eg. proxy_temp_path /spool/nginx/proxy_temp 1 2;"
+        }
+      ]
+    }
+  ]
+}

+ 29 - 29
src/pages/nginx/components/proxy/index.less

@@ -1,29 +1,29 @@
-
-
-.more-conf-popover{
-  .ant-popover-inner-content{
-    width: 100%;
-    padding: 5px;
-  }
-  .more-values.ant-input{
-    min-width: 650px;
-    border: none;
-    color: #333333;
-  }
-
-}
-
-.proxy-settings-input{
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  .less-values{
-    max-width: 250px;
-    overflow: hidden;
-    display: inline-block;
-    line-height: 32px;
-    height: 32px;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-}
+
+
+.more-conf-popover{
+  .ant-popover-inner-content{
+    width: 100%;
+    padding: 5px;
+  }
+  .more-values.ant-input{
+    min-width: 650px;
+    border: none;
+    color: #333333;
+  }
+
+}
+
+.proxy-settings-input{
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  .less-values{
+    max-width: 250px;
+    overflow: hidden;
+    display: inline-block;
+    line-height: 32px;
+    height: 32px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}

+ 87 - 87
src/pages/nginx/components/proxy/index.tsx

@@ -1,87 +1,87 @@
-/**
- * @author tuonian
- * @date 2023/7/5
- */
-import {Button, Drawer, Input, Popover, Tooltip} from "antd";
-import {AdvanceInputConfigs, AutoForm, AutoFormInstance, AutoTypeInputProps, isObject} from 'planning-tools'
-
-import './index.less'
-import {useEffect, useRef, useState} from "react";
-import {EditOutlined} from "@ant-design/icons";
-import {renderProxy} from "./utils.ts";
-import {useAppSelector} from "../../../../store";
-import FormConfig from './config.json'
-
-
-export const ProxySettings = ({value, onChange}: AutoTypeInputProps) => {
-
-  const nginx = useAppSelector(state => state.nginx.current)
-
-  const [data,setData] = useState<any>()
-  const [lines,setLines] = useState<string[]>([])
-  const [open,setOpen] = useState(false)
-
-  const formRef = useRef<AutoFormInstance>()
-
-  useEffect(()=>{
-    if (isObject(value)){
-      setData(value.data)
-      setLines(value.lines || [])
-    }
-    console.log('value change', value)
-  },[value])
-
-  const onSubmitData = async ()=>{
-    if (!nginx?.id){
-      return
-    }
-    const values = await formRef.current?.onSyncSubmit(true);
-    const lines = renderProxy(values, nginx)
-    const postData = {
-      lines: lines,
-      data:  values
-    }
-    onChange?.(postData)
-    setOpen(false)
-  }
-
-  const renderMoreContent = ()=>{
-    if (!lines?.length){
-      return <span>无配置,点击编辑按钮编辑代理设置</span>
-    }
-    return (<Input.TextArea className="more-values"
-                            rows={Math.min(10,lines.length)}
-                            disabled value={lines.join('\n')} />)
-  }
-
-  return (<div className="proxy-settings-input">
-    <Popover
-             overlayClassName="more-conf-popover"
-             destroyTooltipOnHide content={renderMoreContent}>
-      <span className="less-values">{lines.length ? lines.join(' ; ') : '无配置'}</span>
-    </Popover>
-    <Button onClick={()=>setOpen(true)} type="link" icon={<EditOutlined />} />
-    <Drawer title="代理设置"
-            open={open}
-            width={700}
-            onClose={()=>setOpen(false)}
-            rootClassName="proxy-drawer"
-            extra={<>
-              <Tooltip placement="rightBottom" title="提交后,请点击界面保存按钮,保存到服务器">
-                <Button onClick={onSubmitData} type="primary">提交</Button>
-              </Tooltip>
-
-               </>}
-            >
-
-      <AutoForm ref={formRef as never}
-                formProps={{
-                    labelCol:{span: 7}
-                }}
-                data={data}
-                columns={FormConfig.form} />
-    </Drawer>
-    </div>)
-}
-
-AdvanceInputConfigs["proxy_settings"] =ProxySettings
+/**
+ * @author tuonian
+ * @date 2023/7/5
+ */
+import {Button, Drawer, Input, Popover, Tooltip} from "antd";
+import {AdvanceInputConfigs, AutoForm, AutoFormInstance, AutoTypeInputProps, isObject} from 'planning-tools'
+
+import './index.less'
+import {useEffect, useRef, useState} from "react";
+import {EditOutlined} from "@ant-design/icons";
+import {renderProxy} from "./utils.ts";
+import {useAppSelector} from "../../../../store";
+import FormConfig from './config.json'
+
+
+export const ProxySettings = ({value, onChange}: AutoTypeInputProps) => {
+
+  const nginx = useAppSelector(state => state.nginx.current)
+
+  const [data,setData] = useState<any>()
+  const [lines,setLines] = useState<string[]>([])
+  const [open,setOpen] = useState(false)
+
+  const formRef = useRef<AutoFormInstance>()
+
+  useEffect(()=>{
+    if (isObject(value)){
+      setData(value.data)
+      setLines(value.lines || [])
+    }
+    console.log('value change', value)
+  },[value])
+
+  const onSubmitData = async ()=>{
+    if (!nginx?.id){
+      return
+    }
+    const values = await formRef.current?.onSyncSubmit(true);
+    const lines = renderProxy(values, nginx)
+    const postData = {
+      lines: lines,
+      data:  values
+    }
+    onChange?.(postData)
+    setOpen(false)
+  }
+
+  const renderMoreContent = ()=>{
+    if (!lines?.length){
+      return <span>无配置,点击编辑按钮编辑代理设置</span>
+    }
+    return (<Input.TextArea className="more-values"
+                            rows={Math.min(10,lines.length)}
+                            disabled value={lines.join('\n')} />)
+  }
+
+  return (<div className="proxy-settings-input">
+    <Popover
+             overlayClassName="more-conf-popover"
+             destroyTooltipOnHide content={renderMoreContent}>
+      <span className="less-values">{lines.length ? lines.join(' ; ') : '无配置'}</span>
+    </Popover>
+    <Button onClick={()=>setOpen(true)} type="link" icon={<EditOutlined />} />
+    <Drawer title="代理设置"
+            open={open}
+            width={700}
+            onClose={()=>setOpen(false)}
+            rootClassName="proxy-drawer"
+            extra={<>
+              <Tooltip placement="rightBottom" title="提交后,请点击界面保存按钮,保存到服务器">
+                <Button onClick={onSubmitData} type="primary">提交</Button>
+              </Tooltip>
+
+               </>}
+            >
+
+      <AutoForm ref={formRef as never}
+                formProps={{
+                    labelCol:{span: 7}
+                }}
+                data={data}
+                columns={FormConfig.form} />
+    </Drawer>
+    </div>)
+}
+
+AdvanceInputConfigs["proxy_settings"] =ProxySettings

+ 70 - 70
src/pages/nginx/components/proxy/utils.ts

@@ -1,70 +1,70 @@
-import {cloneDeep, isBoolean} from "lodash";
-import {INginx, KeyValue} from "../../../../models/nginx.ts";
-import {isFalse, isNull} from "planning-tools";
-
-/**
- * 渲染代理配置
- * @param data
- * @param nginx
- */
-export const renderProxy = (data: any, nginx: INginx)=>{
-  const lines: string[] = []
-  const values = cloneDeep(data);
-  if (values.proxy_custom_config){
-    lines.push(values.proxy_custom_config)
-  }
-  delete values.proxy_custom_config
-
-  if (Array.isArray(values.proxy_set_header)){
-    values.proxy_set_header.forEach((item: KeyValue)=>{
-      if (isNull(item.value) || isNull(item.name)){
-        return
-      }
-      lines.push(`proxy_set_header    ${item.name}  ${item.value};`)
-    })
-  }
-  delete values.proxy_set_header
-
-  if (values.ssl_certificate){
-    values.proxy_ssl_certificate = `${nginx.dataDir}/certs/${values.ssl_certificate}.pem`
-    values.proxy_ssl_certificate_key = `${nginx.dataDir}/certs/${values.ssl_certificate}.key`
-    delete values.ssl_certificate
-  }
-
-  if (values.tmp_trans_ip){
-    lines.push(`proxy_set_header X-Real-IP $remote_addr;`)
-    lines.push(`proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
-  }
-  delete values.tmp_trans_ip
-  if (values.tmp_trans_host){
-    lines.push(`proxy_set_header Host $host;`)
-  }
-  delete values.tmp_trans_host
-  if (values.tmp_support_ws){
-    lines.push(`proxy_set_header Upgrade $http_upgrade;`)
-    lines.push(`proxy_set_header Connection "upgrade";`)
-  }
-  delete values.tmp_support_ws
-
-  delete values.tmp_proxy_more
-
-  if (isFalse(values.proxy_pass_request_body)){
-    lines.push(`proxy_pass_request_body   off;`)
-    lines.push(`proxy_set_header  Content-Length    "";`)
-  }
-  delete values.proxy_pass_request_body
-
-  Object.keys(values).forEach(k=>{
-    let v = values[k];
-    if (isNull(v)){
-      return
-    }
-    if (isBoolean(v)){
-      v = v ? 'on':'off'
-    }else if (Array.isArray(v)){
-      v = v.join(' ')
-    }
-    lines.push(`${k}  ${v};`)
-  })
-  return lines
-}
+import {cloneDeep, isBoolean} from "lodash";
+import {INginx, KeyValue} from "../../../../models/nginx.ts";
+import {isFalse, isNull} from "planning-tools";
+
+/**
+ * 渲染代理配置
+ * @param data
+ * @param nginx
+ */
+export const renderProxy = (data: any, nginx: INginx)=>{
+  const lines: string[] = []
+  const values = cloneDeep(data);
+  if (values.proxy_custom_config){
+    lines.push(values.proxy_custom_config)
+  }
+  delete values.proxy_custom_config
+
+  if (Array.isArray(values.proxy_set_header)){
+    values.proxy_set_header.forEach((item: KeyValue)=>{
+      if (isNull(item.value) || isNull(item.name)){
+        return
+      }
+      lines.push(`proxy_set_header    ${item.name}  ${item.value};`)
+    })
+  }
+  delete values.proxy_set_header
+
+  if (values.ssl_certificate){
+    values.proxy_ssl_certificate = `${nginx.dataDir}/certs/${values.ssl_certificate}.pem`
+    values.proxy_ssl_certificate_key = `${nginx.dataDir}/certs/${values.ssl_certificate}.key`
+    delete values.ssl_certificate
+  }
+
+  if (values.tmp_trans_ip){
+    lines.push(`proxy_set_header X-Real-IP $remote_addr;`)
+    lines.push(`proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
+  }
+  delete values.tmp_trans_ip
+  if (values.tmp_trans_host){
+    lines.push(`proxy_set_header Host $host;`)
+  }
+  delete values.tmp_trans_host
+  if (values.tmp_support_ws){
+    lines.push(`proxy_set_header Upgrade $http_upgrade;`)
+    lines.push(`proxy_set_header Connection "upgrade";`)
+  }
+  delete values.tmp_support_ws
+
+  delete values.tmp_proxy_more
+
+  if (isFalse(values.proxy_pass_request_body)){
+    lines.push(`proxy_pass_request_body   off;`)
+    lines.push(`proxy_set_header  Content-Length    "";`)
+  }
+  delete values.proxy_pass_request_body
+
+  Object.keys(values).forEach(k=>{
+    let v = values[k];
+    if (isNull(v)){
+      return
+    }
+    if (isBoolean(v)){
+      v = v ? 'on':'off'
+    }else if (Array.isArray(v)){
+      v = v.join(' ')
+    }
+    lines.push(`${k}  ${v};`)
+  })
+  return lines
+}

+ 31 - 31
src/pages/nginx/components/proxypass/index.less

@@ -1,31 +1,31 @@
-.proxy-pass-input{
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: flex-start;
-  .protocol.ant-select{
-    min-width: 80px;
-    width: 80px;
-    max-width: 80px;
-  }
-  .or{
-    margin-left: 5px;
-    margin-right: 5px;
-  }
-  .service-host-input{
-    min-width: 200px;
-    .ant-input{
-      min-width: 200px;
-    }
-  }
-  .upstream-select.ant-select{
-    min-width: 200px;
-    width: auto;
-  }
-}
-
-.proxy-pass-popover{
-  display: flex;
-  flex-direction: row;
-  min-width: 400px;
-}
+.proxy-pass-input{
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-start;
+  .protocol.ant-select{
+    min-width: 80px;
+    width: 80px;
+    max-width: 80px;
+  }
+  .or{
+    margin-left: 5px;
+    margin-right: 5px;
+  }
+  .service-host-input{
+    min-width: 200px;
+    .ant-input{
+      min-width: 200px;
+    }
+  }
+  .upstream-select.ant-select{
+    min-width: 200px;
+    width: auto;
+  }
+}
+
+.proxy-pass-popover{
+  display: flex;
+  flex-direction: row;
+  min-width: 400px;
+}

+ 97 - 97
src/pages/nginx/components/proxypass/index.tsx

@@ -1,97 +1,97 @@
-/**
- * 必须要兼容,手动输入,以及选择负载均衡名字
- * @author tuonian
- * @date 2023/7/4
- */
-import {AutoTypeInputProps, AdvanceInputConfigs} from "planning-tools";
-import {Input, Select} from "antd";
-import {useAppSelector} from "../../../../store";
-import {ChangeEvent, useEffect, useMemo, useState} from "react";
-import './index.less'
-
-const protocols = [{value:"http", label:"http"},{ value: 'https',label: 'https'}]
-
-
-export const ProxyPassInput = ({value, onChange}: AutoTypeInputProps)=>{
-
-  const upstreamServer = useAppSelector(state => state.nginx.upstream);
-
-  const [data,setData] = useState<string>()
-  const [protocol,setProtocol] = useState<string>("http")
-
-  const options = useMemo(()=>{
-    let list:any[] = []
-    if (upstreamServer?.upstreams){
-      list =  upstreamServer.upstreams.map(item=>{
-        return {
-          label: item.name,
-          value: item.name
-        }
-      })
-    }
-    return list
-  },[upstreamServer])
-
-  useEffect(()=>{
-    if (!value || typeof value !=='string'){
-      setData(undefined)
-      return
-    }
-    if (value.startsWith('https://')){
-      setProtocol('https')
-    }else if (value.startsWith('http://')){
-      setProtocol('http')
-    }
-    let pass = value.replace(/http(s)?:\/\//,'');
-    if (pass.endsWith('/')){
-      pass = pass.substring(0,pass.length-1)
-    }
-    if (pass){
-      setData(pass)
-    }
-  },[value])
-
-  const triggerChange = (pro: string,host?: string)=>{
-    if (!pro || !host){
-      return
-    }
-    let pass = `${pro}://${host}`
-    if (!pass.endsWith('/')){
-      pass=pass+'/'
-    }
-    onChange?.(pass)
-  }
-
-  const onProtocolChange = (pro: string)=>{
-    setProtocol(pro)
-    triggerChange(pro,data)
-  }
-
-  const onSelectUpstream = (v?:string)=>{
-    if (v){
-      setData(v)
-      triggerChange(protocol,v)
-    }
-  }
-
-  const userInputChange = (e: ChangeEvent<any>) =>{
-    const v = e.currentTarget.value;
-    setData(v);
-    triggerChange(protocol,v)
-  }
-
-
-
-  return (<div className="proxy-pass-input">
-    <Select value={protocol} onChange={onProtocolChange}
-            className="protocol" options={protocols} />
-    <Input onChange={userInputChange} value={data} allowClear/>
-    <Select onChange={onSelectUpstream}
-            placeholder="选择负载均衡"
-            allowClear
-            className="upstream-select"
-            options={options} />
-    </div>)
-}
-
-AdvanceInputConfigs['proxy_pass'] = ProxyPassInput
+/**
+ * 必须要兼容,手动输入,以及选择负载均衡名字
+ * @author tuonian
+ * @date 2023/7/4
+ */
+import {AutoTypeInputProps, AdvanceInputConfigs} from "planning-tools";
+import {Input, Select} from "antd";
+import {useAppSelector} from "../../../../store";
+import {ChangeEvent, useEffect, useMemo, useState} from "react";
+import './index.less'
+
+const protocols = [{value:"http", label:"http"},{ value: 'https',label: 'https'}]
+
+
+export const ProxyPassInput = ({value, onChange}: AutoTypeInputProps)=>{
+
+  const upstreamServer = useAppSelector(state => state.nginx.upstream);
+
+  const [data,setData] = useState<string>()
+  const [protocol,setProtocol] = useState<string>("http")
+
+  const options = useMemo(()=>{
+    let list:any[] = []
+    if (upstreamServer?.upstreams){
+      list =  upstreamServer.upstreams.map(item=>{
+        return {
+          label: item.name,
+          value: item.name
+        }
+      })
+    }
+    return list
+  },[upstreamServer])
+
+  useEffect(()=>{
+    if (!value || typeof value !=='string'){
+      setData(undefined)
+      return
+    }
+    if (value.startsWith('https://')){
+      setProtocol('https')
+    }else if (value.startsWith('http://')){
+      setProtocol('http')
+    }
+    let pass = value.replace(/http(s)?:\/\//,'');
+    if (pass.endsWith('/')){
+      pass = pass.substring(0,pass.length-1)
+    }
+    if (pass){
+      setData(pass)
+    }
+  },[value])
+
+  const triggerChange = (pro: string,host?: string)=>{
+    if (!pro || !host){
+      return
+    }
+    let pass = `${pro}://${host}`
+    if (!pass.endsWith('/')){
+      pass=pass+'/'
+    }
+    onChange?.(pass)
+  }
+
+  const onProtocolChange = (pro: string)=>{
+    setProtocol(pro)
+    triggerChange(pro,data)
+  }
+
+  const onSelectUpstream = (v?:string)=>{
+    if (v){
+      setData(v)
+      triggerChange(protocol,v)
+    }
+  }
+
+  const userInputChange = (e: ChangeEvent<any>) =>{
+    const v = e.currentTarget.value;
+    setData(v);
+    triggerChange(protocol,v)
+  }
+
+
+
+  return (<div className="proxy-pass-input">
+    <Select value={protocol} onChange={onProtocolChange}
+            className="protocol" options={protocols} />
+    <Input onChange={userInputChange} value={data} allowClear/>
+    <Select onChange={onSelectUpstream}
+            placeholder="选择负载均衡"
+            allowClear
+            className="upstream-select"
+            options={options} />
+    </div>)
+}
+
+AdvanceInputConfigs['proxy_pass'] = ProxyPassInput

+ 70 - 70
src/pages/nginx/components/proxypass/stream.tsx

@@ -1,70 +1,70 @@
-/**
- * 必须要兼容,手动输入,以及选择负载均衡名字
- * @author tuonian
- * @date 2023/7/4
- */
-import {AutoTypeInputProps, AdvanceInputConfigs} from "planning-tools";
-import {Button, Input, Popover, Select} from "antd";
-import {useAppSelector} from "../../../../store";
-import {ChangeEvent, useEffect, useMemo, useState} from "react";
-import {EditOutlined} from "@ant-design/icons";
-
-import './index.less'
-
-export const StreamProxyPassInput = ({value, onChange}: AutoTypeInputProps)=>{
-
-  const upstreamServer = useAppSelector(state => state.nginx.streamUpstream);
-  const [data,setData] = useState<string>()
-
-
-  const options = useMemo(()=>{
-    let list:any[] = []
-    if (upstreamServer?.upstreams){
-      list =  upstreamServer.upstreams.map(item=>{
-        return {
-          label: item.name,
-          value: item.name
-        }
-      })
-    }
-    return list
-  },[upstreamServer])
-
-  useEffect(()=>{
-   setData(value)
-  },[value])
-
-  const onSelectUpstream = (v?:string)=>{
-    if (v){
-      setData(v)
-      onChange?.(v)
-    }
-  }
-
-  const onUserInput = (e:ChangeEvent<any>)=>{
-    setData(e.currentTarget.value)
-    onChange?.(e.currentTarget.value)
-  }
-
-
-
-  const renderInput = ()=>{
-    return (<div className="proxy-pass-popover">
-      <Input placeholder="输入后端服务的IP:PORT或者复制均衡名称" value={data} onChange={onUserInput} />
-      <Select onChange={onSelectUpstream}
-              placeholder="选择负载均衡"
-              allowClear
-              className="upstream-select"
-              options={options} />
-    </div>)
-  }
-
-  return (<>
-    {data}
-    <Popover trigger="click" destroyTooltipOnHide content={renderInput}>
-      <Button type="link" icon={<EditOutlined />} />
-    </Popover>
-  </>)
-}
-
-AdvanceInputConfigs['stream_proxy_pass'] = StreamProxyPassInput
+/**
+ * 必须要兼容,手动输入,以及选择负载均衡名字
+ * @author tuonian
+ * @date 2023/7/4
+ */
+import {AutoTypeInputProps, AdvanceInputConfigs} from "planning-tools";
+import {Button, Input, Popover, Select} from "antd";
+import {useAppSelector} from "../../../../store";
+import {ChangeEvent, useEffect, useMemo, useState} from "react";
+import {EditOutlined} from "@ant-design/icons";
+
+import './index.less'
+
+export const StreamProxyPassInput = ({value, onChange}: AutoTypeInputProps)=>{
+
+  const upstreamServer = useAppSelector(state => state.nginx.streamUpstream);
+  const [data,setData] = useState<string>()
+
+
+  const options = useMemo(()=>{
+    let list:any[] = []
+    if (upstreamServer?.upstreams){
+      list =  upstreamServer.upstreams.map(item=>{
+        return {
+          label: item.name,
+          value: item.name
+        }
+      })
+    }
+    return list
+  },[upstreamServer])
+
+  useEffect(()=>{
+   setData(value)
+  },[value])
+
+  const onSelectUpstream = (v?:string)=>{
+    if (v){
+      setData(v)
+      onChange?.(v)
+    }
+  }
+
+  const onUserInput = (e:ChangeEvent<any>)=>{
+    setData(e.currentTarget.value)
+    onChange?.(e.currentTarget.value)
+  }
+
+
+
+  const renderInput = ()=>{
+    return (<div className="proxy-pass-popover">
+      <Input placeholder="输入后端服务的IP:PORT或者复制均衡名称" value={data} onChange={onUserInput} />
+      <Select onChange={onSelectUpstream}
+              placeholder="选择负载均衡"
+              allowClear
+              className="upstream-select"
+              options={options} />
+    </div>)
+  }
+
+  return (<>
+    {data}
+    <Popover trigger="click" destroyTooltipOnHide content={renderInput}>
+      <Button type="link" icon={<EditOutlined />} />
+    </Popover>
+  </>)
+}
+
+AdvanceInputConfigs['stream_proxy_pass'] = StreamProxyPassInput

+ 335 - 335
src/pages/nginx/components/site/components/Dragger.tsx

@@ -1,336 +1,336 @@
-import React, {useCallback, useEffect, useRef, useState} from "react";
-
-import './dragger.less'
-import {uniqueKey} from "planning-tools";
-import {Button, Tree} from "antd";
-import {
-    InboxOutlined,
-    FileOutlined,
-    FolderOutlined,
-    DownOutlined,
-    DeleteOutlined, LoadingOutlined, UploadOutlined
-} from "@ant-design/icons";
-import type { DataNode } from 'antd/es/tree';
-import {uploadApis} from "../../../../../api/nginx.ts";
-import classNames from "classnames";
-
-/**
- * 文件上传
- */
-type MyFile = {
-
-    children?: MyFile[]
-    /**
-     * 是否为文件夹
-     */
-    isDirectory?: boolean
-    title: string
-    key: string
-    item: FileSystemEntry
-    pKey?: string
-
-} & DataNode
-
-type FileStatus = {
-    success?: boolean
-    error?: boolean
-    loading?: boolean
-    message?: string
-    key: string
-}
-
-type StatusMap = {
-    [key:string]: FileStatus
-}
-
-type TreeNodeProps = {
-    data: MyFile,
-    onDelete: (data: MyFile)=>void,
-    onUpload: (data: MyFile) =>void
-    statusMap: StatusMap
-}
-
-
-const TreeNode = ({data, onDelete, onUpload, statusMap}: TreeNodeProps)=>{
-    const [status,setStatus] = useState<FileStatus>({key: data.key})
-
-    useEffect(()=>{
-        const cur = statusMap[data.key];
-        cur && (setStatus(cur))
-    },[statusMap])
-
-    return (
-        <div className={classNames('node-data',{ 'error': status.error}, { success: status.success})}>
-            <span className="tree-node-title-item">{data.title}</span>
-            <div style={{flex: 1}} />
-            <LoadingOutlined hidden={!status.loading} />
-            <Button onClick={()=>onUpload(data)}
-                    size="small"
-                    type="link"
-                    hidden={status.success || data.isDirectory}
-                    icon={<UploadOutlined />}
-                    />
-            <Button onClick={()=>onDelete(data)}
-                    className="delete-btn"
-                    size="small" type="text"
-                    danger icon={<DeleteOutlined />} />
-        </div>
-    )
-}
-type IProps  = {
-    onComplete?: (key: string, ok?: boolean)=>void
-}
-export const Dragger:React.FC<IProps> = ({onComplete}: IProps) => {
-
-    const [ fileList,setFileList] = useState<MyFile[]>([])
-    const [loading,setLoading] = useState(false)
-    const [batchId,setBatchId] = useState<string>(uniqueKey(15))
-    const [statusMap,setStatusMap] = useState<StatusMap>({})
-    const filesRef = useRef<MyFile[]>([])
-
-    const dropRef = useRef<HTMLDivElement>()
-    const inputRef = useRef<HTMLInputElement>()
-    const fileMapRef = useRef<{[key:string]: MyFile}>({})
-
-
-    const onReset = ()=>{
-        setFileList([])
-        setBatchId(uniqueKey(15))
-        setStatusMap({})
-        fileMapRef.current = {}
-    }
-
-    useEffect(()=>{
-        if (!filesRef.current?.length){
-            return
-        }
-       const okList = filesRef.current.filter(item=>statusMap[item.key]?.success)
-        if (okList.length === filesRef.current.length){
-            onComplete?.(batchId, true)
-        }else {
-            onComplete?.(batchId, false)
-        }
-    },[statusMap])
-
-
-    const  onUploadFile = (data: MyFile)=>{
-        if (data.isDirectory){
-            return Promise.resolve()
-        }
-        const status = {
-            key: data.key,
-            message: '',
-            success: true,
-            error: false,
-            loading: true
-        }
-        setStatusMap(map=>({...map,[data.key]: status}))
-        return uploadApis.uploadFile(data.item as FileSystemFileEntry , batchId)
-            .then(({data: resp})=>{
-                const status = {
-                    key: data.key,
-                    message: '',
-                    success: true,
-                    error: false,
-                    loading: false
-                }
-                setStatusMap(map=>({...map,[data.key]: status}))
-                return resp
-            })
-            .catch(e=>{
-                const status = {
-                    key: data.key,
-                    message: e.message || '上传失败',
-                    success: false,
-                    error: true,
-                    loading: false
-                }
-                setStatusMap(map=>({...map,[data.key]: status}))
-            })
-    }
-    const onUpload = async ()=>{
-        setLoading(true)
-        const allList = Object.values(fileMapRef.current)
-            .filter(item=>!item.isDirectory)
-            .map(item=> ({...item}))
-        filesRef.current = [...allList]
-        const batchUpload = ()=>{
-            const tasks: Promise<any>[] = []
-            for (let i =0;i<4;i++){
-                const data = allList.pop()
-                if (!data){
-                    break
-                }
-                tasks.push(new Promise((resolve,reject) => {
-                    onUploadFile(data)
-                        .then(resp=>resolve(resp))
-                        .catch(e=>reject(e))
-                }))
-            }
-             Promise.all(tasks)
-                .then((data)=>{
-                    console.log('batch result', data)
-                })
-                 .catch(e=>{
-                     console.log('batch result fail', e)
-                 })
-                 .finally(()=>{
-                     if (allList.length){
-                         batchUpload()
-                     }else {
-                         setLoading(false)
-                         onComplete?.(batchId)
-                     }
-                 })
-        }
-
-        batchUpload()
-    }
-
-    const onDelete =(file: MyFile) => {
-        if (!file.pKey){
-            const list = fileList.filter(item=>item.key !== file.key);
-            setFileList(list)
-            return
-        }
-        const parent = fileMapRef.current[file.pKey];
-        if (!parent || !parent.children){
-            return;
-        }
-        parent.children = parent.children.filter(item=>item.key !== file.key)
-        if (!parent.children?.length){
-            const list = fileList.filter(item=>item.key !== parent.key);
-            setFileList(list);
-        }else {
-            setFileList([...fileList])
-        }
-        console.log('file',file, fileMapRef.current)
-    }
-    const appendFiles = (items: DataTransferItemList)=>{
-        const files: MyFile[] = []
-        const fileMap: {[key:string]:MyFile} = {}
-        const id = uniqueKey(15)
-        setBatchId(id)
-        let order = 1
-        const createKey = ()=>{
-            const key = id+'_'+order
-            order++
-            return key
-        }
-
-        const scanFiles = (item: FileSystemEntry, parent?: MyFile)=>{
-            let pList = parent ? parent.children : files;
-            if (!pList){
-                pList = []
-            }
-            if (parent){
-                parent.children = pList
-                fileMap[parent.key] = parent
-            }
-            if (item.isDirectory){
-                const myFile: MyFile = {
-                    title: item.name,
-                    key: createKey(),
-                    children: [],
-                    isDirectory: true,
-                    icon: <FolderOutlined />,
-                    item,
-                    isLeaf: false,
-                    pKey: parent?.key
-                }
-                pList.push(myFile)
-                const reader = (item as FileSystemDirectoryEntry).createReader()
-                reader.readEntries(function (entries){
-                    entries.forEach(entry=>{
-                        scanFiles(entry, myFile)
-                    })
-                })
-            }else{
-                const myFile = {
-                    title: item.name,
-                    key: createKey(),
-                    item,
-                    icon: <FileOutlined />,
-                    isLeaf: true,
-                    pKey: parent?.key,
-                }
-                pList.push(myFile)
-                fileMap[myFile.key] = myFile
-            }
-        }
-
-        for (let i=0;i<items.length;i++){
-            const item = items[i].webkitGetAsEntry();
-            if (!item){
-                continue
-            }
-            scanFiles(item)
-        }
-        setFileList(files)
-        fileMapRef.current = fileMap
-        console.log('files', files)
-    }
-
-    const onDragOver = useCallback((evt: DragEvent)=>{
-        evt.preventDefault()
-    },[])
-
-    const onDropEvent = useCallback((evt: DragEvent)=>{
-        evt.preventDefault()
-        if (!evt.dataTransfer){
-            console.log('onDropEvent dataTransfer is null')
-            return
-        }
-        const items = evt.dataTransfer.items;
-        const files = evt.dataTransfer.files;
-        appendFiles(items)
-        console.log('items', items)
-        console.log('files', files)
-    },[])
-
-    useEffect(()=>{
-        const container = dropRef.current;
-        if (!container){
-            return
-        }
-        container.addEventListener("dragover",onDragOver)
-        container.addEventListener("drop", onDropEvent)
-
-        return ()=>{
-            container.removeEventListener("dragover", onDragOver)
-            container.removeEventListener("drop", onDropEvent)
-        }
-
-    },[])
-
-    return (<div className="dragger-input">
-        <div onClick={()=>inputRef.current?.click()} ref={dropRef as any} className="dragger">
-            <p className="ant-upload-drag-icon">
-                <InboxOutlined />
-            </p>
-            <p className="ant-upload-text">点击或者拖拽文件添加上传的问题</p>
-            <p className="ant-upload-hint">
-               支持单个文件,或者多个文件,将会压缩成tar.xz文件上传到服务端;更新不会备份Nginx服务器上已有的文件,如果初次使用该功能,请注意文件备份,更新的文件在服务端和nginx的服务器均有备份
-            </p>
-        </div>
-        <input hidden ref={inputRef as any} type="file"/>
-        <div className="file-tree-title btn-list">
-            <span className="list-name">文件列表</span>
-            <Button danger type="link" size="small" disabled={loading} hidden={!fileList.length} onClick={onReset}>清空</Button>
-            <Button type="link" size="small" loading={loading} hidden={!fileList.length} onClick={onUpload} >上传</Button>
-        </div>
-        <Tree
-            showIcon
-            showLine
-            blockNode
-            switcherIcon={<DownOutlined />}
-            autoExpandParent
-            defaultExpandAll
-            titleRender={(data)=>(<TreeNode onUpload={onUploadFile}
-                                            statusMap={statusMap}
-                                            data={data}
-                                            onDelete={onDelete} />)}
-            treeData={fileList} />
-
-    </div>)
+import React, {useCallback, useEffect, useRef, useState} from "react";
+
+import './dragger.less'
+import {uniqueKey} from "planning-tools";
+import {Button, Tree} from "antd";
+import {
+    InboxOutlined,
+    FileOutlined,
+    FolderOutlined,
+    DownOutlined,
+    DeleteOutlined, LoadingOutlined, UploadOutlined
+} from "@ant-design/icons";
+import type { DataNode } from 'antd/es/tree';
+import {uploadApis} from "../../../../../api/nginx.ts";
+import classNames from "classnames";
+
+/**
+ * 文件上传
+ */
+type MyFile = {
+
+    children?: MyFile[]
+    /**
+     * 是否为文件夹
+     */
+    isDirectory?: boolean
+    title: string
+    key: string
+    item: FileSystemEntry
+    pKey?: string
+
+} & DataNode
+
+type FileStatus = {
+    success?: boolean
+    error?: boolean
+    loading?: boolean
+    message?: string
+    key: string
+}
+
+type StatusMap = {
+    [key:string]: FileStatus
+}
+
+type TreeNodeProps = {
+    data: MyFile,
+    onDelete: (data: MyFile)=>void,
+    onUpload: (data: MyFile) =>void
+    statusMap: StatusMap
+}
+
+
+const TreeNode = ({data, onDelete, onUpload, statusMap}: TreeNodeProps)=>{
+    const [status,setStatus] = useState<FileStatus>({key: data.key})
+
+    useEffect(()=>{
+        const cur = statusMap[data.key];
+        cur && (setStatus(cur))
+    },[statusMap])
+
+    return (
+        <div className={classNames('node-data',{ 'error': status.error}, { success: status.success})}>
+            <span className="tree-node-title-item">{data.title}</span>
+            <div style={{flex: 1}} />
+            <LoadingOutlined hidden={!status.loading} />
+            <Button onClick={()=>onUpload(data)}
+                    size="small"
+                    type="link"
+                    hidden={status.success || data.isDirectory}
+                    icon={<UploadOutlined />}
+                    />
+            <Button onClick={()=>onDelete(data)}
+                    className="delete-btn"
+                    size="small" type="text"
+                    danger icon={<DeleteOutlined />} />
+        </div>
+    )
+}
+type IProps  = {
+    onComplete?: (key: string, ok?: boolean)=>void
+}
+export const Dragger:React.FC<IProps> = ({onComplete}: IProps) => {
+
+    const [ fileList,setFileList] = useState<MyFile[]>([])
+    const [loading,setLoading] = useState(false)
+    const [batchId,setBatchId] = useState<string>(uniqueKey(15))
+    const [statusMap,setStatusMap] = useState<StatusMap>({})
+    const filesRef = useRef<MyFile[]>([])
+
+    const dropRef = useRef<HTMLDivElement>()
+    const inputRef = useRef<HTMLInputElement>()
+    const fileMapRef = useRef<{[key:string]: MyFile}>({})
+
+
+    const onReset = ()=>{
+        setFileList([])
+        setBatchId(uniqueKey(15))
+        setStatusMap({})
+        fileMapRef.current = {}
+    }
+
+    useEffect(()=>{
+        if (!filesRef.current?.length){
+            return
+        }
+       const okList = filesRef.current.filter(item=>statusMap[item.key]?.success)
+        if (okList.length === filesRef.current.length){
+            onComplete?.(batchId, true)
+        }else {
+            onComplete?.(batchId, false)
+        }
+    },[statusMap])
+
+
+    const  onUploadFile = (data: MyFile)=>{
+        if (data.isDirectory){
+            return Promise.resolve()
+        }
+        const status = {
+            key: data.key,
+            message: '',
+            success: true,
+            error: false,
+            loading: true
+        }
+        setStatusMap(map=>({...map,[data.key]: status}))
+        return uploadApis.uploadFile(data.item as FileSystemFileEntry , batchId)
+            .then(({data: resp})=>{
+                const status = {
+                    key: data.key,
+                    message: '',
+                    success: true,
+                    error: false,
+                    loading: false
+                }
+                setStatusMap(map=>({...map,[data.key]: status}))
+                return resp
+            })
+            .catch(e=>{
+                const status = {
+                    key: data.key,
+                    message: e.message || '上传失败',
+                    success: false,
+                    error: true,
+                    loading: false
+                }
+                setStatusMap(map=>({...map,[data.key]: status}))
+            })
+    }
+    const onUpload = async ()=>{
+        setLoading(true)
+        const allList = Object.values(fileMapRef.current)
+            .filter(item=>!item.isDirectory)
+            .map(item=> ({...item}))
+        filesRef.current = [...allList]
+        const batchUpload = ()=>{
+            const tasks: Promise<any>[] = []
+            for (let i =0;i<4;i++){
+                const data = allList.pop()
+                if (!data){
+                    break
+                }
+                tasks.push(new Promise((resolve,reject) => {
+                    onUploadFile(data)
+                        .then(resp=>resolve(resp))
+                        .catch(e=>reject(e))
+                }))
+            }
+             Promise.all(tasks)
+                .then((data)=>{
+                    console.log('batch result', data)
+                })
+                 .catch(e=>{
+                     console.log('batch result fail', e)
+                 })
+                 .finally(()=>{
+                     if (allList.length){
+                         batchUpload()
+                     }else {
+                         setLoading(false)
+                         onComplete?.(batchId)
+                     }
+                 })
+        }
+
+        batchUpload()
+    }
+
+    const onDelete =(file: MyFile) => {
+        if (!file.pKey){
+            const list = fileList.filter(item=>item.key !== file.key);
+            setFileList(list)
+            return
+        }
+        const parent = fileMapRef.current[file.pKey];
+        if (!parent || !parent.children){
+            return;
+        }
+        parent.children = parent.children.filter(item=>item.key !== file.key)
+        if (!parent.children?.length){
+            const list = fileList.filter(item=>item.key !== parent.key);
+            setFileList(list);
+        }else {
+            setFileList([...fileList])
+        }
+        console.log('file',file, fileMapRef.current)
+    }
+    const appendFiles = (items: DataTransferItemList)=>{
+        const files: MyFile[] = []
+        const fileMap: {[key:string]:MyFile} = {}
+        const id = uniqueKey(15)
+        setBatchId(id)
+        let order = 1
+        const createKey = ()=>{
+            const key = id+'_'+order
+            order++
+            return key
+        }
+
+        const scanFiles = (item: FileSystemEntry, parent?: MyFile)=>{
+            let pList = parent ? parent.children : files;
+            if (!pList){
+                pList = []
+            }
+            if (parent){
+                parent.children = pList
+                fileMap[parent.key] = parent
+            }
+            if (item.isDirectory){
+                const myFile: MyFile = {
+                    title: item.name,
+                    key: createKey(),
+                    children: [],
+                    isDirectory: true,
+                    icon: <FolderOutlined />,
+                    item,
+                    isLeaf: false,
+                    pKey: parent?.key
+                }
+                pList.push(myFile)
+                const reader = (item as FileSystemDirectoryEntry).createReader()
+                reader.readEntries(function (entries){
+                    entries.forEach(entry=>{
+                        scanFiles(entry, myFile)
+                    })
+                })
+            }else{
+                const myFile = {
+                    title: item.name,
+                    key: createKey(),
+                    item,
+                    icon: <FileOutlined />,
+                    isLeaf: true,
+                    pKey: parent?.key,
+                }
+                pList.push(myFile)
+                fileMap[myFile.key] = myFile
+            }
+        }
+
+        for (let i=0;i<items.length;i++){
+            const item = items[i].webkitGetAsEntry();
+            if (!item){
+                continue
+            }
+            scanFiles(item)
+        }
+        setFileList(files)
+        fileMapRef.current = fileMap
+        console.log('files', files)
+    }
+
+    const onDragOver = useCallback((evt: DragEvent)=>{
+        evt.preventDefault()
+    },[])
+
+    const onDropEvent = useCallback((evt: DragEvent)=>{
+        evt.preventDefault()
+        if (!evt.dataTransfer){
+            console.log('onDropEvent dataTransfer is null')
+            return
+        }
+        const items = evt.dataTransfer.items;
+        const files = evt.dataTransfer.files;
+        appendFiles(items)
+        console.log('items', items)
+        console.log('files', files)
+    },[])
+
+    useEffect(()=>{
+        const container = dropRef.current;
+        if (!container){
+            return
+        }
+        container.addEventListener("dragover",onDragOver)
+        container.addEventListener("drop", onDropEvent)
+
+        return ()=>{
+            container.removeEventListener("dragover", onDragOver)
+            container.removeEventListener("drop", onDropEvent)
+        }
+
+    },[])
+
+    return (<div className="dragger-input">
+        <div onClick={()=>inputRef.current?.click()} ref={dropRef as any} className="dragger">
+            <p className="ant-upload-drag-icon">
+                <InboxOutlined />
+            </p>
+            <p className="ant-upload-text">点击或者拖拽文件添加上传的问题</p>
+            <p className="ant-upload-hint">
+               支持单个文件,或者多个文件,将会压缩成tar.xz文件上传到服务端;更新不会备份Nginx服务器上已有的文件,如果初次使用该功能,请注意文件备份,更新的文件在服务端和nginx的服务器均有备份
+            </p>
+        </div>
+        <input hidden ref={inputRef as any} type="file"/>
+        <div className="file-tree-title btn-list">
+            <span className="list-name">文件列表</span>
+            <Button danger type="link" size="small" disabled={loading} hidden={!fileList.length} onClick={onReset}>清空</Button>
+            <Button type="link" size="small" loading={loading} hidden={!fileList.length} onClick={onUpload} >上传</Button>
+        </div>
+        <Tree
+            showIcon
+            showLine
+            blockNode
+            switcherIcon={<DownOutlined />}
+            autoExpandParent
+            defaultExpandAll
+            titleRender={(data)=>(<TreeNode onUpload={onUploadFile}
+                                            statusMap={statusMap}
+                                            data={data}
+                                            onDelete={onDelete} />)}
+            treeData={fileList} />
+
+    </div>)
 }

+ 59 - 59
src/pages/nginx/components/site/components/dragger.less

@@ -1,59 +1,59 @@
-.dragger-input{
-
-  .dragger{
-    border: dashed #efefef 1px;
-    padding: 10px 20px;
-    background: #f6f5f5;
-    cursor: pointer;
-    text-align: center;
-    .ant-upload-drag-icon .anticon{
-      font-size: 35px;
-      color: #1e88c7;
-    }
-  }
-  .ant-tree-list{
-    margin-top: 10px;
-    display: flex;
-    flex-direction: column;
-    flex-wrap: nowrap;
-    .ant-tree-node-content-wrapper{
-      display: flex;
-      .ant-tree-title{
-        flex: 1;
-        .node-data{
-          width: 100%;
-          display: flex;
-          flex-direction: row;
-          align-items: center;
-          flex-wrap: nowrap;
-          .tree-node-title-item{
-            display: block;
-          }
-        }
-      }
-    }
-  }
-
-  .node-data.success{
-    color: #1e88c7;
-  }
-  .node-data.error{
-    color: red;
-  }
-  .btn-list{
-    margin-top: 10px;
-    .ant-btn+.ant-btn{
-      margin-left: 10px;
-    }
-  }
-  .file-tree-title{
-    .list-name{
-      font-size: 14px;
-      font-weight: bold;
-      margin-right: 10px;
-    }
-    border-bottom: solid 1px #efefef;
-    padding-bottom: 5px;
-  }
-}
-
+.dragger-input{
+
+  .dragger{
+    border: dashed #efefef 1px;
+    padding: 10px 20px;
+    background: #f6f5f5;
+    cursor: pointer;
+    text-align: center;
+    .ant-upload-drag-icon .anticon{
+      font-size: 35px;
+      color: #1e88c7;
+    }
+  }
+  .ant-tree-list{
+    margin-top: 10px;
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+    .ant-tree-node-content-wrapper{
+      display: flex;
+      .ant-tree-title{
+        flex: 1;
+        .node-data{
+          width: 100%;
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          flex-wrap: nowrap;
+          .tree-node-title-item{
+            display: block;
+          }
+        }
+      }
+    }
+  }
+
+  .node-data.success{
+    color: #1e88c7;
+  }
+  .node-data.error{
+    color: red;
+  }
+  .btn-list{
+    margin-top: 10px;
+    .ant-btn+.ant-btn{
+      margin-left: 10px;
+    }
+  }
+  .file-tree-title{
+    .list-name{
+      font-size: 14px;
+      font-weight: bold;
+      margin-right: 10px;
+    }
+    border-bottom: solid 1px #efefef;
+    padding-bottom: 5px;
+  }
+}
+

+ 25 - 25
src/pages/nginx/components/site/index.less

@@ -1,25 +1,25 @@
-.site-deploy{
-
-  .ant-drawer-header{
-    padding: 5px 10px;
-  }
-  .ant-drawer-body{
-    padding: 10px 15px;
-  }
-  .btn-list{
-    .ant-btn+.ant-btn{
-      margin-left: 10px;
-    }
-  }
-  .inline-item{
-    .ant-form-item-row{
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-      .ant-form-item-label{
-        margin-bottom: 0;
-        padding-bottom: 0;
-      }
-    }
-  }
-}
+.site-deploy{
+
+  .ant-drawer-header{
+    padding: 5px 10px;
+  }
+  .ant-drawer-body{
+    padding: 10px 15px;
+  }
+  .btn-list{
+    .ant-btn+.ant-btn{
+      margin-left: 10px;
+    }
+  }
+  .inline-item{
+    .ant-form-item-row{
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      .ant-form-item-label{
+        margin-bottom: 0;
+        padding-bottom: 0;
+      }
+    }
+  }
+}

+ 123 - 123
src/pages/nginx/components/site/index.tsx

@@ -1,123 +1,123 @@
-import {Button, Drawer, Form, Input, Switch} from "antd";
-import {
-    Message
-} from "planning-tools";
-import { useState} from "react";
-import {EditOutlined} from "@ant-design/icons";
-
-
-import './index.less'
-import {INginxLocation} from "../../../../models/nginx.ts";
-import {Dragger} from "./components/Dragger.tsx";
-import {IDeployReq, uploadApis} from "../../../../api/nginx.ts";
-import {useAppSelector} from "../../../../store";
-
-type IProps = {
-    location: INginxLocation
-}
-
-/**
- * 路由,站点,规则编辑
- * @param value
- * @param onChange
- * @param column
- * @constructor
- */
-export const SiteInput = ({ location }: IProps) => {
-
-    const [editData,setEditData] = useState<Partial<IDeployReq>>()
-
-    const [complete,setComplete] = useState(false)
-    const [loading,setLoading] = useState(false)
-    const [form] = Form.useForm()
-
-    const nginx = useAppSelector(state => state.nginx.current);
-
-
-    const onAddData = ()=>{
-        if (!location || !nginx?.id){
-            return
-        }
-        if (location.proxy_type!=='static'){
-            Message.warning('只支持静态站点的部署!')
-            return;
-        }
-        const initialData :Partial<IDeployReq> = {
-            nginxId: nginx.id,
-            clear: false
-        }
-        if (location.alias){
-            initialData.dir = location.alias
-        }else if (location.root){
-            initialData.dir = location.root
-        }
-        setEditData(initialData)
-    }
-
-    const onSubmitData = async ()=>{
-      if (!nginx?.id){
-          return
-      }
-      const values = await form.validateFields()
-        console.log('onSubmitData',values)
-
-      setLoading(true)
-        const postData: IDeployReq = {
-          key: "",
-            dir:"",
-          ...editData,
-            ...values,
-          nginxId: nginx.id,
-        }
-      uploadApis.deploy(postData)
-          .then(()=>{
-              Message.success('部署成功!')
-          })
-          .finally(()=>{
-              setLoading(false)
-          })
-    }
-
-    /**
-     * 文件上传完成的回调
-     * @param batchId
-     * @param finish 是否全部上传完成
-     */
-    const onUploadComplete =(batchId: string, finish?: boolean)=>{
-        setEditData(data=>({...data,key: batchId}))
-        setComplete(!!finish)
-    }
-
-    return (
-        <>
-            <Button onClick={()=>onAddData()} className="add-btn" type="link" icon={<EditOutlined/>}/>
-            <Drawer title={"静态资源部署"}
-                    placement="right"
-                    open={!!editData}
-                    onClose={() => setEditData(undefined)}
-                    destroyOnClose
-                    width={650}
-                    className="site-deploy"
-            >
-                <Form form={form} layout="vertical" initialValues={editData}>
-                    <Form.Item name="dir" label="部署目录" rules={[{required: true,message: '请完善部署目录',validateTrigger: 'blur'}]}>
-                        <Input />
-                    </Form.Item>
-                    <Form.Item className="inline-item"
-                               labelCol={{span: 4}}
-                               wrapperCol={{span: 18}}
-                               name="clear" label="全量部署" tooltip={{title: "全量部署会删除已有的文件,请注意"}}>
-                        <Switch />
-                    </Form.Item>
-                    <Form.Item name="files" label="资源更新">
-                        <Dragger onComplete={onUploadComplete}/>
-                    </Form.Item>
-                    <div className="btn-list">
-                        <Button disabled={loading} onClick={()=>setEditData(undefined)}>取消</Button>
-                        <Button loading={loading} onClick={onSubmitData} danger disabled={!complete}>部署</Button>
-                    </div>
-                </Form>
-            </Drawer>
-        </>
-    )
-}
+import {Button, Drawer, Form, Input, Switch} from "antd";
+import {
+    Message
+} from "planning-tools";
+import { useState} from "react";
+import {EditOutlined} from "@ant-design/icons";
+
+
+import './index.less'
+import {INginxLocation} from "../../../../models/nginx.ts";
+import {Dragger} from "./components/Dragger.tsx";
+import {IDeployReq, uploadApis} from "../../../../api/nginx.ts";
+import {useAppSelector} from "../../../../store";
+
+type IProps = {
+    location: INginxLocation
+}
+
+/**
+ * 路由,站点,规则编辑
+ * @param value
+ * @param onChange
+ * @param column
+ * @constructor
+ */
+export const SiteInput = ({ location }: IProps) => {
+
+    const [editData,setEditData] = useState<Partial<IDeployReq>>()
+
+    const [complete,setComplete] = useState(false)
+    const [loading,setLoading] = useState(false)
+    const [form] = Form.useForm()
+
+    const nginx = useAppSelector(state => state.nginx.current);
+
+
+    const onAddData = ()=>{
+        if (!location || !nginx?.id){
+            return
+        }
+        if (location.proxy_type!=='static'){
+            Message.warning('只支持静态站点的部署!')
+            return;
+        }
+        const initialData :Partial<IDeployReq> = {
+            nginxId: nginx.id,
+            clear: false
+        }
+        if (location.alias){
+            initialData.dir = location.alias
+        }else if (location.root){
+            initialData.dir = location.root
+        }
+        setEditData(initialData)
+    }
+
+    const onSubmitData = async ()=>{
+      if (!nginx?.id){
+          return
+      }
+      const values = await form.validateFields()
+        console.log('onSubmitData',values)
+
+      setLoading(true)
+        const postData: IDeployReq = {
+          key: "",
+            dir:"",
+          ...editData,
+            ...values,
+          nginxId: nginx.id,
+        }
+      uploadApis.deploy(postData)
+          .then(()=>{
+              Message.success('部署成功!')
+          })
+          .finally(()=>{
+              setLoading(false)
+          })
+    }
+
+    /**
+     * 文件上传完成的回调
+     * @param batchId
+     * @param finish 是否全部上传完成
+     */
+    const onUploadComplete =(batchId: string, finish?: boolean)=>{
+        setEditData(data=>({...data,key: batchId}))
+        setComplete(!!finish)
+    }
+
+    return (
+        <>
+            <Button onClick={()=>onAddData()} className="add-btn" type="link" icon={<EditOutlined/>}/>
+            <Drawer title={"静态资源部署"}
+                    placement="right"
+                    open={!!editData}
+                    onClose={() => setEditData(undefined)}
+                    destroyOnClose
+                    width={650}
+                    className="site-deploy"
+            >
+                <Form form={form} layout="vertical" initialValues={editData}>
+                    <Form.Item name="dir" label="部署目录" rules={[{required: true,message: '请完善部署目录',validateTrigger: 'blur'}]}>
+                        <Input />
+                    </Form.Item>
+                    <Form.Item className="inline-item"
+                               labelCol={{span: 4}}
+                               wrapperCol={{span: 18}}
+                               name="clear" label="全量部署" tooltip={{title: "全量部署会删除已有的文件,请注意"}}>
+                        <Switch />
+                    </Form.Item>
+                    <Form.Item name="files" label="资源更新">
+                        <Dragger onComplete={onUploadComplete}/>
+                    </Form.Item>
+                    <div className="btn-list">
+                        <Button disabled={loading} onClick={()=>setEditData(undefined)}>取消</Button>
+                        <Button loading={loading} onClick={onSubmitData} danger disabled={!complete}>部署</Button>
+                    </div>
+                </Form>
+            </Drawer>
+        </>
+    )
+}

+ 42 - 42
src/pages/nginx/config.tsx

@@ -1,42 +1,42 @@
-/**
- * @author tuonian
- * @date 2023/6/29
- */
-import {useAppSelector} from "../../store";
-import {useMemo} from "react";
-import formConfig from '../../config/nginx_form.json'
-import {INginxFormConfig, INginxFormTemplate} from "../../models/nginx.ts";
-import formTemplate from '../../config/nginx_template.json'
-import {cloneDeep} from "lodash";
-
-// eslint-disable-next-line react-refresh/only-export-components
-const EmptyConfig: INginxFormConfig = {
-  server: [],
-  location: [],
-  addNginx: [],
-  nginxSettings: [],
-  nginxConf: [],
-  upstream: [],
-  stream: []
-}
-
-
-export const useFormConfig = () => {
-  const fixConfig = useAppSelector(state => state.nginx.formConfig);
-  return  useMemo(()=>{
-    const config = fixConfig || formConfig;
-    if (!config){
-      return EmptyConfig
-    }
-    return config as INginxFormConfig
-  },[formConfig, fixConfig])
-}
-
-/**
- * 数据模板
- */
-export const useFormTemplate = ()=>{
-  return useMemo(()=>{
-    return cloneDeep(formTemplate) as INginxFormTemplate
-  },[])
-}
+/**
+ * @author tuonian
+ * @date 2023/6/29
+ */
+import {useAppSelector} from "../../store";
+import {useMemo} from "react";
+import formConfig from '../../config/nginx_form.json'
+import {INginxFormConfig, INginxFormTemplate} from "../../models/nginx.ts";
+import formTemplate from '../../config/nginx_template.json'
+import {cloneDeep} from "lodash";
+
+// eslint-disable-next-line react-refresh/only-export-components
+const EmptyConfig: INginxFormConfig = {
+  server: [],
+  location: [],
+  addNginx: [],
+  nginxSettings: [],
+  nginxConf: [],
+  upstream: [],
+  stream: []
+}
+
+
+export const useFormConfig = () => {
+  const fixConfig = useAppSelector(state => state.nginx.formConfig);
+  return  useMemo(()=>{
+    const config = fixConfig || formConfig;
+    if (!config){
+      return EmptyConfig
+    }
+    return config as INginxFormConfig
+  },[formConfig, fixConfig])
+}
+
+/**
+ * 数据模板
+ */
+export const useFormTemplate = ()=>{
+  return useMemo(()=>{
+    return cloneDeep(formTemplate) as INginxFormTemplate
+  },[])
+}

+ 63 - 63
src/pages/nginx/help/args.mdx

@@ -1,63 +1,63 @@
-```nginx中可用的变量```\
-$args                    #请求中的参数值\
-$query_string            #同 $args\
-$arg_NAME                #GET请求中NAME的值\
-$is_args                 #如果请求中有参数,值为"?",否则为空字符串\
-$uri                     #请求中的当前URI(不带请求参数,参数位于$args),可以不同于浏览器传递的$request_uri的值,它可以通过内部重定向,或者使用index指令进行修改,$uri不包含主机名,如"/foo/bar.html"。\
-$document_uri            #同 $uri\
-$document_root           #当前请求的文档根目录或别名\
-$host                    #优先级:HTTP请求行的主机名>"HOST"请求头字段>符合请求的服务器名.请求中的主机头字段,如果请求中的主机头不可用,则为服务器处理请求的服务器名称\
-$hostname                #主机名\
-$https                   #如果开启了SSL安全模式,值为"on",否则为空字符串。\
-$binary_remote_addr      #客户端地址的二进制形式,固定长度为4个字节\
-$body_bytes_sent         #传输给客户端的字节数,响应头不计算在内;这个变量和Apache的mod_log_config模块中的"%B"参数保持兼容\
-$bytes_sent              #传输给客户端的字节数\
-$connection              #TCP连接的序列号\
-$connection_requests     #TCP连接当前的请求数量\
-$content_length          #"Content-Length" 请求头字段\
-$content_type            #"Content-Type" 请求头字段\
-$cookie_name             #cookie名称\
-$limit_rate              #用于设置响应的速度限制\
-$msec                    #当前的Unix时间戳\
-$nginx_version           #nginx版本\
-$pid                     #工作进程的PID\
-$pipe                    #如果请求来自管道通信,值为"p",否则为"."\
-$proxy_protocol_addr     #获取代理访问服务器的客户端地址,如果是直接访问,该值为空字符串\
-$realpath_root           #当前请求的文档根目录或别名的真实路径,会将所有符号连接转换为真实路径\
-$remote_addr             #客户端地址\
-$remote_port             #客户端端口\
-$remote_user             #用于HTTP基础认证服务的用户名\
-$request                 #代表客户端的请求地址\
-$request_body            #客户端的请求主体:此变量可在location中使用,将请求主体通过proxy_pass,fastcgi_pass,uwsgi_pass和scgi_pass传递给下一级的代理服务器\
-$request_body_file       #将客户端请求主体保存在临时文件中。文件处理结束后,此文件需删除。如果需要之一开启此功能,需要设置client_body_in_file_only。如果将次文件传 递给后端的代理服务器,需要禁用request body,即设置proxy_pass_request_body off,fastcgi_pass_request_body off,uwsgi_pass_request_body off,or scgi_pass_request_body off\
-$request_completion      #如果请求成功,值为"OK",如果请求未完成或者请求不是一个范围请求的最后一部分,则为空\
-$request_filename        #当前连接请求的文件路径,由root或alias指令与URI请求生成\
-$request_length          #请求的长度 (包括请求的地址,http请求头和请求主体)\
-$request_method          #HTTP请求方法,通常为"GET"或"POST"\
-$request_time            #处理客户端请求使用的时间,单位为秒,精度毫秒; 从读入客户端的第一个字节开始,直到把最后一个字符发送给客户端后进行日志写入为止。\
-$request_uri             #这个变量等于包含一些客户端请求参数的原始URI,它无法修改,请查看$uri更改或重写URI,不包含主机名,例如:"/cnphp/test.php?arg=freemouse"\
-$scheme                  #请求使用的Web协议,"http" 或 "https"\
-$server_addr             #服务器端地址,需要注意的是:为了避免访问linux系统内核,应将ip地址提前设置在配置文件中\
-$server_name             #服务器名\
-$server_port             #服务器端口\
-$server_protocol         #服务器的HTTP版本,通常为 "HTTP/1.0" 或 "HTTP/1.1"\
-$status                  #HTTP响应代码\
-$time_iso8601            #服务器时间的ISO 8610格式\
-$time_local              #服务器时间(LOG Format 格式)\
-$cookie_NAME             #客户端请求Header头中的cookie变量,前缀"$cookie_"加上cookie名称的变量,该变量的值即为cookie名称的值\
-$http_NAME               #匹配任意请求头字段;变量名中的后半部分NAME可以替换成任意请求头字段,如在配置文件中需要获取http请求头:"Accept-Language",$http_accept_language即可\
-$http_cookie                                    \
-$http_host               #请求地址,即浏览器中你输入的地址(IP或域名)\
-$http_referer            #url跳转来源,用来记录从那个页面链接访问过来的\
-$http_user_agent         #用户终端浏览器等信息\
-$http_x_forwarded_for                                                           \
-$sent_http_NAME          #可以设置任意http响应头字段;变量名中的后半部分NAME可以替换成任意响应头字段,如需要设置响应头Content-length,$sent_http_content_length即可\
-$sent_http_cache_control                    \
-$sent_http_connection                      \                                 
-$sent_http_content_type          \
-$sent_http_keep_alive           \
-$sent_http_last_modified            \
-$sent_http_location             \
-$sent_http_transfer_encoding            \
-
-来源于[博客]()
+```nginx中可用的变量```\
+$args                    #请求中的参数值\
+$query_string            #同 $args\
+$arg_NAME                #GET请求中NAME的值\
+$is_args                 #如果请求中有参数,值为"?",否则为空字符串\
+$uri                     #请求中的当前URI(不带请求参数,参数位于$args),可以不同于浏览器传递的$request_uri的值,它可以通过内部重定向,或者使用index指令进行修改,$uri不包含主机名,如"/foo/bar.html"。\
+$document_uri            #同 $uri\
+$document_root           #当前请求的文档根目录或别名\
+$host                    #优先级:HTTP请求行的主机名>"HOST"请求头字段>符合请求的服务器名.请求中的主机头字段,如果请求中的主机头不可用,则为服务器处理请求的服务器名称\
+$hostname                #主机名\
+$https                   #如果开启了SSL安全模式,值为"on",否则为空字符串。\
+$binary_remote_addr      #客户端地址的二进制形式,固定长度为4个字节\
+$body_bytes_sent         #传输给客户端的字节数,响应头不计算在内;这个变量和Apache的mod_log_config模块中的"%B"参数保持兼容\
+$bytes_sent              #传输给客户端的字节数\
+$connection              #TCP连接的序列号\
+$connection_requests     #TCP连接当前的请求数量\
+$content_length          #"Content-Length" 请求头字段\
+$content_type            #"Content-Type" 请求头字段\
+$cookie_name             #cookie名称\
+$limit_rate              #用于设置响应的速度限制\
+$msec                    #当前的Unix时间戳\
+$nginx_version           #nginx版本\
+$pid                     #工作进程的PID\
+$pipe                    #如果请求来自管道通信,值为"p",否则为"."\
+$proxy_protocol_addr     #获取代理访问服务器的客户端地址,如果是直接访问,该值为空字符串\
+$realpath_root           #当前请求的文档根目录或别名的真实路径,会将所有符号连接转换为真实路径\
+$remote_addr             #客户端地址\
+$remote_port             #客户端端口\
+$remote_user             #用于HTTP基础认证服务的用户名\
+$request                 #代表客户端的请求地址\
+$request_body            #客户端的请求主体:此变量可在location中使用,将请求主体通过proxy_pass,fastcgi_pass,uwsgi_pass和scgi_pass传递给下一级的代理服务器\
+$request_body_file       #将客户端请求主体保存在临时文件中。文件处理结束后,此文件需删除。如果需要之一开启此功能,需要设置client_body_in_file_only。如果将次文件传 递给后端的代理服务器,需要禁用request body,即设置proxy_pass_request_body off,fastcgi_pass_request_body off,uwsgi_pass_request_body off,or scgi_pass_request_body off\
+$request_completion      #如果请求成功,值为"OK",如果请求未完成或者请求不是一个范围请求的最后一部分,则为空\
+$request_filename        #当前连接请求的文件路径,由root或alias指令与URI请求生成\
+$request_length          #请求的长度 (包括请求的地址,http请求头和请求主体)\
+$request_method          #HTTP请求方法,通常为"GET"或"POST"\
+$request_time            #处理客户端请求使用的时间,单位为秒,精度毫秒; 从读入客户端的第一个字节开始,直到把最后一个字符发送给客户端后进行日志写入为止。\
+$request_uri             #这个变量等于包含一些客户端请求参数的原始URI,它无法修改,请查看$uri更改或重写URI,不包含主机名,例如:"/cnphp/test.php?arg=freemouse"\
+$scheme                  #请求使用的Web协议,"http" 或 "https"\
+$server_addr             #服务器端地址,需要注意的是:为了避免访问linux系统内核,应将ip地址提前设置在配置文件中\
+$server_name             #服务器名\
+$server_port             #服务器端口\
+$server_protocol         #服务器的HTTP版本,通常为 "HTTP/1.0" 或 "HTTP/1.1"\
+$status                  #HTTP响应代码\
+$time_iso8601            #服务器时间的ISO 8610格式\
+$time_local              #服务器时间(LOG Format 格式)\
+$cookie_NAME             #客户端请求Header头中的cookie变量,前缀"$cookie_"加上cookie名称的变量,该变量的值即为cookie名称的值\
+$http_NAME               #匹配任意请求头字段;变量名中的后半部分NAME可以替换成任意请求头字段,如在配置文件中需要获取http请求头:"Accept-Language",$http_accept_language即可\
+$http_cookie                                    \
+$http_host               #请求地址,即浏览器中你输入的地址(IP或域名)\
+$http_referer            #url跳转来源,用来记录从那个页面链接访问过来的\
+$http_user_agent         #用户终端浏览器等信息\
+$http_x_forwarded_for                                                           \
+$sent_http_NAME          #可以设置任意http响应头字段;变量名中的后半部分NAME可以替换成任意响应头字段,如需要设置响应头Content-length,$sent_http_content_length即可\
+$sent_http_cache_control                    \
+$sent_http_connection                      \                                 
+$sent_http_content_type          \
+$sent_http_keep_alive           \
+$sent_http_last_modified            \
+$sent_http_location             \
+$sent_http_transfer_encoding            \
+
+来源于[博客]()

+ 22 - 22
src/pages/nginx/help/index.less

@@ -1,22 +1,22 @@
-.help-container{
-  padding: 0 15px;
-  overflow: hidden;
-  height: 100%;
-  .ant-tabs {
-    height: 100%;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    .ant-tabs-nav{
-      margin-bottom: 0;
-    }
-    .ant-tabs-content-holder{
-      flex: 1;
-      overflow: hidden;
-      .ant-tabs-content,.ant-tabs-tabpane{
-        height: 100%;
-        overflow: auto;
-      }
-    }
-  }
-}
+.help-container{
+  padding: 0 15px;
+  overflow: hidden;
+  height: 100%;
+  .ant-tabs {
+    height: 100%;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    .ant-tabs-nav{
+      margin-bottom: 0;
+    }
+    .ant-tabs-content-holder{
+      flex: 1;
+      overflow: hidden;
+      .ant-tabs-content,.ant-tabs-tabpane{
+        height: 100%;
+        overflow: auto;
+      }
+    }
+  }
+}

+ 25 - 25
src/pages/nginx/help/index.tsx

@@ -1,25 +1,25 @@
-/**
- * @author tuonian
- * @date 2023/7/10
- */
-
-import Args from './args.mdx'
-
-import './index.less'
-import {Tabs} from "antd";
-
-export const HelpPage = ()=>{
-
-  const items =[{
-    key: 'args',
-    label: '变量',
-    children: <Args />
-  }]
-
-
-  return (<div className="help-container">
-    <Tabs
-      items={items}
-      />
-  </div>)
-}
+/**
+ * @author tuonian
+ * @date 2023/7/10
+ */
+
+import Args from './args.mdx'
+
+import './index.less'
+import {Tabs} from "antd";
+
+export const HelpPage = ()=>{
+
+  const items =[{
+    key: 'args',
+    label: '变量',
+    children: <Args />
+  }]
+
+
+  return (<div className="help-container">
+    <Tabs
+      items={items}
+      />
+  </div>)
+}

+ 71 - 71
src/pages/nginx/http/components/HttpConfSync.tsx

@@ -1,71 +1,71 @@
-/**
- * @author tuonian
- * @date 2023/7/6
- */
-import {INginx} from "../../../../models/nginx.ts";
-import {Button, Drawer, Input, Tooltip} from "antd";
-import {ChangeEvent, useEffect, useState} from "react";
-import './index.less'
-import {SyncOutlined} from "@ant-design/icons";
-import {NginxApis} from "../../../../api/nginx.ts";
-import {useAppDispatch} from "../../../../store";
-import {NginxActions} from "../../../../store/slice/nginx.ts";
-import {Message} from "planning-tools";
-
-type IProps = {
-  nginx?: INginx
-}
-export const HttpConfSync = ({nginx}: IProps)=>{
-
-  const [value,setValue] = useState<string>()
-  const [open,setOpen] = useState(false)
-  const [loading,setLoading] = useState(false)
-
-  const dispatch = useAppDispatch()
-
-
-  useEffect(()=>{
-    setValue(nginx?.httpConf)
-  },[nginx])
-
-  const onChange = (evt: ChangeEvent<HTMLTextAreaElement>)=>{
-    setValue(evt.currentTarget.value)
-  }
-
-  const onSubmitData = ()=>{
-    if (!nginx?.id){
-      return
-    }
-    setLoading(true);
-    NginxApis.refreshHttp({ id: nginx.id, httpConf: value || '', httpData: nginx.httpData})
-      .then(()=>{
-        dispatch(NginxActions.updateNginx({...nginx, httpConf: value}))
-        Message.success("success")
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
-
-  if (!nginx?.id){
-    return null
-  }
-
-  return (<>
-    <Button onClick={()=>setOpen(true)}>配置文件</Button>
-    <Drawer title="nginx.conf"
-            open={open}
-            destroyOnClose
-            onClose={()=>setOpen(false)}
-            width={750}
-            className="nginx-conf-drawer"
-            extra={<><Tooltip placement="leftBottom" title={`同步配置文件,注意:直接修改配置文件,将在界面操作“同步”功能后丢失`}>
-              <Button loading={loading} onClick={onSubmitData} icon={<SyncOutlined />}/>
-            </Tooltip></>}
-            >
-      <Input.TextArea onChange={onChange} value={value}  />
-    </Drawer>
-    </>)
-
-
-}
+/**
+ * @author tuonian
+ * @date 2023/7/6
+ */
+import {INginx} from "../../../../models/nginx.ts";
+import {Button, Drawer, Input, Tooltip} from "antd";
+import {ChangeEvent, useEffect, useState} from "react";
+import './index.less'
+import {SyncOutlined} from "@ant-design/icons";
+import {NginxApis} from "../../../../api/nginx.ts";
+import {useAppDispatch} from "../../../../store";
+import {NginxActions} from "../../../../store/slice/nginx.ts";
+import {Message} from "planning-tools";
+
+type IProps = {
+  nginx?: INginx
+}
+export const HttpConfSync = ({nginx}: IProps)=>{
+
+  const [value,setValue] = useState<string>()
+  const [open,setOpen] = useState(false)
+  const [loading,setLoading] = useState(false)
+
+  const dispatch = useAppDispatch()
+
+
+  useEffect(()=>{
+    setValue(nginx?.httpConf)
+  },[nginx])
+
+  const onChange = (evt: ChangeEvent<HTMLTextAreaElement>)=>{
+    setValue(evt.currentTarget.value)
+  }
+
+  const onSubmitData = ()=>{
+    if (!nginx?.id){
+      return
+    }
+    setLoading(true);
+    NginxApis.refreshHttp({ id: nginx.id, httpConf: value || '', httpData: nginx.httpData})
+      .then(()=>{
+        dispatch(NginxActions.updateNginx({...nginx, httpConf: value}))
+        Message.success("success")
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+  }
+
+  if (!nginx?.id){
+    return null
+  }
+
+  return (<>
+    <Button onClick={()=>setOpen(true)}>配置文件</Button>
+    <Drawer title="nginx.conf"
+            open={open}
+            destroyOnClose
+            onClose={()=>setOpen(false)}
+            width={750}
+            className="nginx-conf-drawer"
+            extra={<><Tooltip placement="leftBottom" title={`同步配置文件,注意:直接修改配置文件,将在界面操作“同步”功能后丢失`}>
+              <Button loading={loading} onClick={onSubmitData} icon={<SyncOutlined />}/>
+            </Tooltip></>}
+            >
+      <Input.TextArea onChange={onChange} value={value}  />
+    </Drawer>
+    </>)
+
+
+}

+ 26 - 26
src/pages/nginx/http/components/index.less

@@ -1,26 +1,26 @@
-.nginx-conf-drawer{
-  height: 100%;
-  overflow: hidden;
-  .ant-drawer-content-wrapper{
-    height: 100%;
-    overflow: hidden;
-  }
-  .ant-drawer-wrapper-body{
-    height: 100%;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-  }
-  .ant-drawer-header{
-    padding: 8px 10px;
-  }
-  .ant-drawer-body{
-    padding: 10px;
-    flex: 1;
-    overflow-y: hidden;
-    textarea.ant-input{
-      height: 100%;
-    }
-  }
-
-}
+.nginx-conf-drawer{
+  height: 100%;
+  overflow: hidden;
+  .ant-drawer-content-wrapper{
+    height: 100%;
+    overflow: hidden;
+  }
+  .ant-drawer-wrapper-body{
+    height: 100%;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+  .ant-drawer-header{
+    padding: 8px 10px;
+  }
+  .ant-drawer-body{
+    padding: 10px;
+    flex: 1;
+    overflow-y: hidden;
+    textarea.ant-input{
+      height: 100%;
+    }
+  }
+
+}

+ 126 - 126
src/pages/nginx/http/index.tsx

@@ -1,126 +1,126 @@
-/**
- * @author tuonian
- * @date 2023/6/29
- */
-import {useFormConfig, useFormTemplate} from "../config.tsx";
-import {AutoForm, AutoFormInstance, Message} from "planning-tools";
-import {useEffect, useRef, useState} from "react";
-import {Button, Tooltip} from "antd";
-import {useAppDispatch, useAppSelector} from "../../../store";
-import {cloneDeep} from "lodash";
-import {NginxApis} from "../../../api/nginx.ts";
-import {HttpConfSync} from "./components/HttpConfSync.tsx";
-import {NginxActions} from "../../../store/slice/nginx.ts";
-import {QuestionCircleOutlined} from "@ant-design/icons";
-import {toNginxConf} from "./utils.ts";
-
-export const NginxHttp = () => {
-
-  const formConfig = useFormConfig()
-  const formTemplate = useFormTemplate();
-
-  const [data,setData] = useState<any>({})
-  const formRef = useRef<AutoFormInstance>()
-  const nginx = useAppSelector(state => state.nginx.current);
-  const [loading,setLoading] = useState(false)
-
-  const dispatch = useAppDispatch()
-
-  const onSubmitForm = async (sync?: boolean)=>{
-    if (!nginx){
-      return
-    }
-    const values = await formRef.current?.onSyncSubmit(true);
-    const saveData = { ...data, ...values };
-    console.log('saveData', saveData)
-    setData(saveData)
-    if (sync){
-      onSyncHttpConf(saveData)
-      return
-    }
-
-    const postData = cloneDeep(nginx);
-    postData.httpData = JSON.stringify(saveData);
-    postData.httpConf = "";
-    setLoading(false)
-    dispatch(NginxActions.updateNginx(postData))
-    NginxApis.updateOrAdd(postData)
-        .then(()=>{
-          Message.success('保存成功!')
-        })
-        .finally(()=>{
-          setLoading(false)
-        })
-  }
-
-
-  /**
-   * 将配置文件同步到服务器
-   */
-  const onSyncHttpConf = (nginxData: any) => {
-    if (!nginx?.id){
-      return
-    }
-    const nginxConf = toNginxConf(nginx, nginxData)
-    const postData = {
-      id: nginx.id,
-      httpConf: nginxConf,
-      // HttpData
-      httpData: JSON.stringify(nginxData)
-    }
-    setLoading(true);
-    dispatch(NginxActions.updateNginx({
-      ...nginx,
-      httpConf: nginxConf,
-    }))
-    NginxApis.refreshHttp(postData)
-      .then(()=>{
-        Message.success('sync success!');
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
-
-  useEffect(()=>{
-    const updateData = { ...formTemplate.nginxConf, ...data };
-    setData(updateData)
-    formRef.current?.setData(updateData)
-  },[formTemplate])
-
-  useEffect(()=>{
-    if (!nginx?.id){
-      return
-    }
-    try {
-      const curData = JSON.parse(nginx.httpData)
-      const updateData = { ...formTemplate.nginxConf, ...curData };
-      setData(updateData)
-      formRef.current?.setData(updateData)
-    }catch (e){
-      console.log('parse httpData fail',e)
-    }
-    console.log('nginx change', nginx)
-  },[nginx])
-
-
-  return (<div className="page">
-    <div className="page-header">
-      <span>nginx.conf配置</span>
-      <div style={{flex:1}} />
-      <HttpConfSync nginx={nginx} />
-      <Button danger loading={loading} onClick={() => onSubmitForm(true)}>
-        同步
-        <Tooltip placement="left" title="同步配置文件到服务器,如果该server为禁用状态,将从服务器删除该配置文件">
-          <QuestionCircleOutlined />
-        </Tooltip>
-      </Button>
-      <Button loading={loading} onClick={()=>onSubmitForm(false)}>保存</Button>
-    </div>
-    <div className="page-container">
-      <AutoForm ref={formRef as any} columns={formConfig.nginxConf} data={data} />
-    </div>
-
-
-  </div>)
-}
+/**
+ * @author tuonian
+ * @date 2023/6/29
+ */
+import {useFormConfig, useFormTemplate} from "../config.tsx";
+import {AutoForm, AutoFormInstance, Message} from "planning-tools";
+import {useEffect, useRef, useState} from "react";
+import {Button, Tooltip} from "antd";
+import {useAppDispatch, useAppSelector} from "../../../store";
+import {cloneDeep} from "lodash";
+import {NginxApis} from "../../../api/nginx.ts";
+import {HttpConfSync} from "./components/HttpConfSync.tsx";
+import {NginxActions} from "../../../store/slice/nginx.ts";
+import {QuestionCircleOutlined} from "@ant-design/icons";
+import {toNginxConf} from "./utils.ts";
+
+export const NginxHttp = () => {
+
+  const formConfig = useFormConfig()
+  const formTemplate = useFormTemplate();
+
+  const [data,setData] = useState<any>({})
+  const formRef = useRef<AutoFormInstance>()
+  const nginx = useAppSelector(state => state.nginx.current);
+  const [loading,setLoading] = useState(false)
+
+  const dispatch = useAppDispatch()
+
+  const onSubmitForm = async (sync?: boolean)=>{
+    if (!nginx){
+      return
+    }
+    const values = await formRef.current?.onSyncSubmit(true);
+    const saveData = { ...data, ...values };
+    console.log('saveData', saveData)
+    setData(saveData)
+    if (sync){
+      onSyncHttpConf(saveData)
+      return
+    }
+
+    const postData = cloneDeep(nginx);
+    postData.httpData = JSON.stringify(saveData);
+    postData.httpConf = "";
+    setLoading(false)
+    dispatch(NginxActions.updateNginx(postData))
+    NginxApis.updateOrAdd(postData)
+        .then(()=>{
+          Message.success('保存成功!')
+        })
+        .finally(()=>{
+          setLoading(false)
+        })
+  }
+
+
+  /**
+   * 将配置文件同步到服务器
+   */
+  const onSyncHttpConf = (nginxData: any) => {
+    if (!nginx?.id){
+      return
+    }
+    const nginxConf = toNginxConf(nginx, nginxData)
+    const postData = {
+      id: nginx.id,
+      httpConf: nginxConf,
+      // HttpData
+      httpData: JSON.stringify(nginxData)
+    }
+    setLoading(true);
+    dispatch(NginxActions.updateNginx({
+      ...nginx,
+      httpConf: nginxConf,
+    }))
+    NginxApis.refreshHttp(postData)
+      .then(()=>{
+        Message.success('sync success!');
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+  }
+
+  useEffect(()=>{
+    const updateData = { ...formTemplate.nginxConf, ...data };
+    setData(updateData)
+    formRef.current?.setData(updateData)
+  },[formTemplate])
+
+  useEffect(()=>{
+    if (!nginx?.id){
+      return
+    }
+    try {
+      const curData = JSON.parse(nginx.httpData)
+      const updateData = { ...formTemplate.nginxConf, ...curData };
+      setData(updateData)
+      formRef.current?.setData(updateData)
+    }catch (e){
+      console.log('parse httpData fail',e)
+    }
+    console.log('nginx change', nginx)
+  },[nginx])
+
+
+  return (<div className="page">
+    <div className="page-header">
+      <span>nginx.conf配置</span>
+      <div style={{flex:1}} />
+      <HttpConfSync nginx={nginx} />
+      <Button danger loading={loading} onClick={() => onSubmitForm(true)}>
+        同步
+        <Tooltip placement="left" title="同步配置文件到服务器,如果该server为禁用状态,将从服务器删除该配置文件">
+          <QuestionCircleOutlined />
+        </Tooltip>
+      </Button>
+      <Button loading={loading} onClick={()=>onSubmitForm(false)}>保存</Button>
+    </div>
+    <div className="page-container">
+      <AutoForm ref={formRef as any} columns={formConfig.nginxConf} data={data} />
+    </div>
+
+
+  </div>)
+}

+ 143 - 143
src/pages/nginx/http/utils.ts

@@ -1,143 +1,143 @@
-import {isBoolean, isNull, isObject} from "lodash";
-import {INginx} from "../../../models/nginx.ts";
-import {isBasicData} from "planning-tools";
-import {isNgxModuleValue, NgxModuleData} from "../components/input.ts";
-
-const excludeKeys =["other","tmp","temp","key","proxy_settings"]
-
-/**
- * 对一些特殊的值做一些特别的处理
- */
-const valueProcessor: {[key:string]: (value:any) => string |string[]} = {
-    log_format: (values: any[]) => {
-        return values.map(v=>`${v.name}     ${v.content}`)
-    },
-    access_log: (values:any) => `${values.path}     ${values.name}`
-}
-
-/**
- * 以.分割key,重新组织结构
- * @param parent 给定上级的对象
- * @param pKey 上级的key, 包含后面的. ,比如一个完整的key 为 http.log_format.name ,则该值依次为 “”,http.,http.log_format.
- * @param key 当前级的key,以上为例,则依次为 http.log_format.name,log_format.name, name
- * @param values 所有的values,全部是键值对,不考虑嵌套的情况
- */
-const fillChildValues = (parent: any, pKey:string, key: string, values:any) => {
-    const dot = key.indexOf('.')
-    if (dot > 0 && dot <key.length-1){
-        const isFill = values[`${pKey}${key}`];
-        if (isFill || isNull(isFill)){
-            const firstKey = key.substring(0,dot)
-            const childKey = key.substring(dot+1)
-            let pValue: any =parent[firstKey];
-            if (!pValue || !isObject(pValue)){
-                pValue = {}
-            }
-            parent[firstKey] = pValue;
-            fillChildValues(pValue,`${pKey}${firstKey}.`,childKey, values)
-        }else {
-            console.log('[http] skip key ', pKey,key, isFill)
-        }
-    }else {
-        let kv =  values[`${pKey}${key}`]
-        if (isNull(kv)){
-            return
-        }
-        if (valueProcessor[key]){
-            kv = valueProcessor[key](kv)
-        }else if (isBoolean(kv)){
-            kv = kv ? 'on':'off'
-        }
-        parent[key] = kv;
-    }
-}
-
-/**
- * 有遇见以层级分结构的,比如http.abc,则判断有无http的值,如果为false,则跳过该顶级字段
- * @param values
- */
-const toNginxObj = (values: any) => {
-    const nginxObj: any = {};
-    Object.keys(values).forEach(k=>{
-        if (excludeKeys.indexOf(k) > -1){
-            return
-        }
-        const value = values[k];
-        if (isNull(value)){
-            return;
-        }
-        fillChildValues(nginxObj,"",k,values)
-    })
-    return nginxObj
-}
-
-/**
- * 将值增加到lines里面
- * @param prefix
- * @param lines
- * @param key
- * @param value
- */
-export const append2Lines = (prefix: string,lines: string[],key: string, value: any)=>{
-  // NgxModuleData , lines必须包含分号;
-  if (isNgxModuleValue(value)){
-    (value as NgxModuleData).lines?.forEach((line:string)=>{
-      lines.push(`${prefix}${line}`)
-    })
-  }else if (isBasicData(value)){
-    lines.push(`${prefix}${key}  ${value};`)
-  }else if (Array.isArray(value)){
-    value.forEach((line:string)=>{
-      lines.push(`${prefix}${key}  ${line};`)
-    })
-  }else {
-    console.log('[render http], skip for ', key, value)
-  }
-}
-export const toNginxConf = ( nginx: INginx, data: any)=>{
-    const nginxObj: any = toNginxObj(data)
-    const lines: string[] = [];
-    lines.push(`user  ${nginxObj.user || 'nginx'};`)
-    lines.push(`worker_processes  ${nginxObj.worker_processes || 'auto'};`)
-    lines.push(`error_log  ${nginxObj.error_log};`)
-    lines.push(`pid        ${nginxObj.pid || '/var/run/nginx.pid'};`)
-    lines.push(`events {`)
-    if (nginxObj.events){
-        Object.keys(nginxObj.events).forEach(k=>{
-          const value = nginxObj.events[k]
-          append2Lines('    ', lines,k,value)
-        })
-    }
-    lines.push('}')
-    lines.push(`http {`)
-    if (nginxObj.http){
-        Object.keys(nginxObj.http).forEach(k=>{
-          const value = nginxObj.http[k] as never
-          if (k === 'more'){
-            lines.push(value)
-            return;
-          }
-          if (isNull(value)){
-            return
-          }
-          append2Lines('    ', lines, k,value)
-        })
-    }
-
-    lines.push(`    include ${nginx.dataDir}/conf.d/*.conf;`)
-    // lines.push(`    include ${nginx.nginxDir}/conf.d/*.conf;`)
-    lines.push('}')
-
-    lines.push(`stream {`)
-    if (nginxObj.stream){
-        Object.keys(nginxObj.stream).forEach(k=>{
-          const value = nginxObj.stream[k]
-          append2Lines(`    `, lines,k,value)
-        })
-        lines.push(`    include ${nginx.dataDir}/stream.d/*.conf;`)
-        // lines.push(`    include ${nginx.nginxDir}/stream.d/*.conf;`)
-    }
-    lines.push('}')
-    return lines.join('\n')
-}
-
+import {isBoolean, isNull, isObject} from "lodash";
+import {INginx} from "../../../models/nginx.ts";
+import {isBasicData} from "planning-tools";
+import {isNgxModuleValue, NgxModuleData} from "../components/input.ts";
+
+const excludeKeys =["other","tmp","temp","key","proxy_settings"]
+
+/**
+ * 对一些特殊的值做一些特别的处理
+ */
+const valueProcessor: {[key:string]: (value:any) => string |string[]} = {
+    log_format: (values: any[]) => {
+        return values.map(v=>`${v.name}     ${v.content}`)
+    },
+    access_log: (values:any) => `${values.path}     ${values.name}`
+}
+
+/**
+ * 以.分割key,重新组织结构
+ * @param parent 给定上级的对象
+ * @param pKey 上级的key, 包含后面的. ,比如一个完整的key 为 http.log_format.name ,则该值依次为 “”,http.,http.log_format.
+ * @param key 当前级的key,以上为例,则依次为 http.log_format.name,log_format.name, name
+ * @param values 所有的values,全部是键值对,不考虑嵌套的情况
+ */
+const fillChildValues = (parent: any, pKey:string, key: string, values:any) => {
+    const dot = key.indexOf('.')
+    if (dot > 0 && dot <key.length-1){
+        const isFill = values[`${pKey}${key}`];
+        if (isFill || isNull(isFill)){
+            const firstKey = key.substring(0,dot)
+            const childKey = key.substring(dot+1)
+            let pValue: any =parent[firstKey];
+            if (!pValue || !isObject(pValue)){
+                pValue = {}
+            }
+            parent[firstKey] = pValue;
+            fillChildValues(pValue,`${pKey}${firstKey}.`,childKey, values)
+        }else {
+            console.log('[http] skip key ', pKey,key, isFill)
+        }
+    }else {
+        let kv =  values[`${pKey}${key}`]
+        if (isNull(kv)){
+            return
+        }
+        if (valueProcessor[key]){
+            kv = valueProcessor[key](kv)
+        }else if (isBoolean(kv)){
+            kv = kv ? 'on':'off'
+        }
+        parent[key] = kv;
+    }
+}
+
+/**
+ * 有遇见以层级分结构的,比如http.abc,则判断有无http的值,如果为false,则跳过该顶级字段
+ * @param values
+ */
+const toNginxObj = (values: any) => {
+    const nginxObj: any = {};
+    Object.keys(values).forEach(k=>{
+        if (excludeKeys.indexOf(k) > -1){
+            return
+        }
+        const value = values[k];
+        if (isNull(value)){
+            return;
+        }
+        fillChildValues(nginxObj,"",k,values)
+    })
+    return nginxObj
+}
+
+/**
+ * 将值增加到lines里面
+ * @param prefix
+ * @param lines
+ * @param key
+ * @param value
+ */
+export const append2Lines = (prefix: string,lines: string[],key: string, value: any)=>{
+  // NgxModuleData , lines必须包含分号;
+  if (isNgxModuleValue(value)){
+    (value as NgxModuleData).lines?.forEach((line:string)=>{
+      lines.push(`${prefix}${line}`)
+    })
+  }else if (isBasicData(value)){
+    lines.push(`${prefix}${key}  ${value};`)
+  }else if (Array.isArray(value)){
+    value.forEach((line:string)=>{
+      lines.push(`${prefix}${key}  ${line};`)
+    })
+  }else {
+    console.log('[render http], skip for ', key, value)
+  }
+}
+export const toNginxConf = ( nginx: INginx, data: any)=>{
+    const nginxObj: any = toNginxObj(data)
+    const lines: string[] = [];
+    lines.push(`user  ${nginxObj.user || 'nginx'};`)
+    lines.push(`worker_processes  ${nginxObj.worker_processes || 'auto'};`)
+    lines.push(`error_log  ${nginxObj.error_log};`)
+    lines.push(`pid        ${nginxObj.pid || '/var/run/nginx.pid'};`)
+    lines.push(`events {`)
+    if (nginxObj.events){
+        Object.keys(nginxObj.events).forEach(k=>{
+          const value = nginxObj.events[k]
+          append2Lines('    ', lines,k,value)
+        })
+    }
+    lines.push('}')
+    lines.push(`http {`)
+    if (nginxObj.http){
+        Object.keys(nginxObj.http).forEach(k=>{
+          const value = nginxObj.http[k] as never
+          if (k === 'more'){
+            lines.push(value)
+            return;
+          }
+          if (isNull(value)){
+            return
+          }
+          append2Lines('    ', lines, k,value)
+        })
+    }
+
+    lines.push(`    include ${nginx.dataDir}/conf.d/*.conf;`)
+    // lines.push(`    include ${nginx.nginxDir}/conf.d/*.conf;`)
+    lines.push('}')
+
+    lines.push(`stream {`)
+    if (nginxObj.stream){
+        Object.keys(nginxObj.stream).forEach(k=>{
+          const value = nginxObj.stream[k]
+          append2Lines(`    `, lines,k,value)
+        })
+        lines.push(`    include ${nginx.dataDir}/stream.d/*.conf;`)
+        // lines.push(`    include ${nginx.nginxDir}/stream.d/*.conf;`)
+    }
+    lines.push('}')
+    return lines.join('\n')
+}
+

+ 37 - 37
src/pages/nginx/index.less

@@ -1,37 +1,37 @@
-.nginx-container{
-  height: 100%;
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  .nginx-header{
-    border-bottom: solid 1px #efefef;
-    font-weight: bolder;
-    font-size: 20px;
-    padding: 10px 15px;
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    box-sizing: border-box;
-    height: 50px;
-  }
-
-  .ant-menu{
-    height: 100%;
-  }
-  .nginx-conf{
-    flex: 1;
-    display: flex;
-    flex-direction: row;
-    overflow: hidden;
-    width: 100%;
-    .nginx-routes{
-      flex: 1;
-      width: 100%;
-      overflow: hidden;
-      .error{
-        padding: 20px;
-      }
-    }
-  }
-}
+.nginx-container{
+  height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  .nginx-header{
+    border-bottom: solid 1px #efefef;
+    font-weight: bolder;
+    font-size: 20px;
+    padding: 10px 15px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    box-sizing: border-box;
+    height: 50px;
+  }
+
+  .ant-menu{
+    height: 100%;
+  }
+  .nginx-conf{
+    flex: 1;
+    display: flex;
+    flex-direction: row;
+    overflow: hidden;
+    width: 100%;
+    .nginx-routes{
+      flex: 1;
+      width: 100%;
+      overflow: hidden;
+      .error{
+        padding: 20px;
+      }
+    }
+  }
+}

+ 131 - 131
src/pages/nginx/index.tsx

@@ -1,131 +1,131 @@
-/**
- * @author tuonian
- * @date 2023/6/26
- */
-import {useLocation, useParams, useNavigate, Outlet} from "react-router";
-import {useEffect, useMemo, useState} from "react";
-import {useAppDispatch, useAppSelector} from "../../store";
-
-import './index.less'
-import type { MenuProps } from 'antd';
-import { Menu } from 'antd';
-import {createNginxMenus, serverRoute} from "../../routes/routes";
-import {NginxRouteParams} from "./types.ts";
-import {NginxActions} from "../../store/slice/nginx.ts";
-import {NavLink} from "react-router-dom";
-import {BackButton} from "../../components/BackButton.tsx";
-import {StopStartButton} from "./components/StopStartButton.tsx";
-import './components'
-
-/**
- * nginx配置首页
- * @param children
- * @constructor
- */
-export const Nginx = ({children}: any)=>{
-
-  const location = useLocation();
-  const params = useParams<NginxRouteParams>()
-  const navigate = useNavigate()
-  const current = useAppSelector(state => state.nginx.current);
-  const server = useAppSelector(state => state.nginx.server);
-  const servers = useAppSelector(state => state.nginx.servers)
-
-  const [activeKey,setActiveKey] = useState<string>('settings')
-  const [openKeys,setOpenKeys] = useState<string[]>([])
-
-  const dispatch = useAppDispatch();
-
-
-  console.log('children',children,params)
-
-  useEffect(()=>{
-    if (!current || String(current.id) != params.id){
-      navigate(-1)
-    }else {
-      console.log('nginx data', current, params.id)
-    }
-  },[params.id])
-
-  useEffect(()=>{
-    setActiveKey(location.pathname)
-    console.log('location changed ', location)
-  },[location.pathname])
-
-  useEffect(()=>{
-    if (!current || !params.sid){
-      return
-    }
-    const routeKey = serverRoute(current.id, params.sid);
-    if (openKeys.indexOf(routeKey) == -1){
-      setOpenKeys(openKeys.concat([routeKey]))
-    }
-    if (params.sid && params.sid !== String(server?.id)){
-      const now = servers.find(item=>String(item.id) === params.sid);
-      if (!now){
-        console.log('nginx server invalidate sid', server, params)
-        dispatch(NginxActions.setServer())
-      }else {
-        dispatch(NginxActions.setServer(now))
-      }
-    }else if (!params.sid){
-      dispatch(NginxActions.setServer())
-    }
-    console.log('params change', params, location, routeKey)
-  },[params, servers, current])
-
-  const onClick: MenuProps['onClick'] = (e) => {
-    setActiveKey(e.key);
-    navigate(e.key, {
-      replace: true,
-    })
-
-    console.log('click ', e);
-  };
-
-  /**
-   * 仅允许一个打开
-   * @param e
-   */
-  const onOpenChange: MenuProps['onOpenChange'] = (e)=>{
-    console.log('onOpenChange', e)
-    setOpenKeys(e)
-  }
-
-  const menuItems: MenuProps['items'] = useMemo(()=>{
-    if (!current){
-      return []
-    }
-    return createNginxMenus(current, servers);
-  },[current, servers])
-
-  return (<div className="nginx-container">
-    <div className="nginx-header">
-      <BackButton to="/"/>
-      <div>Nginx实例配置:{current?.name}</div>
-      <div>({current?.ipAddr || '--'})</div>
-      <div style={{flex: 1}} />
-      <StopStartButton />
-      <a target="_blank" style={{fontSize: 14,marginLeft: 10}} href="https://nginx.org/en/docs/">参考文档</a>
-    </div>
-    <div className="nginx-conf">
-      <Menu
-        onClick={onClick}
-        style={{ width: 256 }}
-        mode="inline"
-        items={menuItems}
-        activeKey={activeKey}
-        selectedKeys={[activeKey]}
-        onOpenChange={onOpenChange}
-        openKeys={openKeys}
-      />
-      <div className="nginx-routes">
-        {
-          !current ? (<div className="error">该实例不存在或者已被删除,<NavLink to="/">返回首页</NavLink></div> ): null
-        }
-        <Outlet />
-      </div>
-    </div>
-
-  </div>)
-}
+/**
+ * @author tuonian
+ * @date 2023/6/26
+ */
+import {useLocation, useParams, useNavigate, Outlet} from "react-router";
+import {useEffect, useMemo, useState} from "react";
+import {useAppDispatch, useAppSelector} from "../../store";
+
+import './index.less'
+import type { MenuProps } from 'antd';
+import { Menu } from 'antd';
+import {createNginxMenus, serverRoute} from "../../routes/routes";
+import {NginxRouteParams} from "./types.ts";
+import {NginxActions} from "../../store/slice/nginx.ts";
+import {NavLink} from "react-router-dom";
+import {BackButton} from "../../components/BackButton.tsx";
+import {StopStartButton} from "./components/StopStartButton.tsx";
+import './components'
+
+/**
+ * nginx配置首页
+ * @param children
+ * @constructor
+ */
+export const Nginx = ({children}: any)=>{
+
+  const location = useLocation();
+  const params = useParams<NginxRouteParams>()
+  const navigate = useNavigate()
+  const current = useAppSelector(state => state.nginx.current);
+  const server = useAppSelector(state => state.nginx.server);
+  const servers = useAppSelector(state => state.nginx.servers)
+
+  const [activeKey,setActiveKey] = useState<string>('settings')
+  const [openKeys,setOpenKeys] = useState<string[]>([])
+
+  const dispatch = useAppDispatch();
+
+
+  console.log('children',children,params)
+
+  useEffect(()=>{
+    if (!current || String(current.id) != params.id){
+      navigate(-1)
+    }else {
+      console.log('nginx data', current, params.id)
+    }
+  },[params.id])
+
+  useEffect(()=>{
+    setActiveKey(location.pathname)
+    console.log('location changed ', location)
+  },[location.pathname])
+
+  useEffect(()=>{
+    if (!current || !params.sid){
+      return
+    }
+    const routeKey = serverRoute(current.id, params.sid);
+    if (openKeys.indexOf(routeKey) == -1){
+      setOpenKeys(openKeys.concat([routeKey]))
+    }
+    if (params.sid && params.sid !== String(server?.id)){
+      const now = servers.find(item=>String(item.id) === params.sid);
+      if (!now){
+        console.log('nginx server invalidate sid', server, params)
+        dispatch(NginxActions.setServer())
+      }else {
+        dispatch(NginxActions.setServer(now))
+      }
+    }else if (!params.sid){
+      dispatch(NginxActions.setServer())
+    }
+    console.log('params change', params, location, routeKey)
+  },[params, servers, current])
+
+  const onClick: MenuProps['onClick'] = (e) => {
+    setActiveKey(e.key);
+    navigate(e.key, {
+      replace: true,
+    })
+
+    console.log('click ', e);
+  };
+
+  /**
+   * 仅允许一个打开
+   * @param e
+   */
+  const onOpenChange: MenuProps['onOpenChange'] = (e)=>{
+    console.log('onOpenChange', e)
+    setOpenKeys(e)
+  }
+
+  const menuItems: MenuProps['items'] = useMemo(()=>{
+    if (!current){
+      return []
+    }
+    return createNginxMenus(current, servers);
+  },[current, servers])
+
+  return (<div className="nginx-container">
+    <div className="nginx-header">
+      <BackButton to="/"/>
+      <div>Nginx实例配置:{current?.name}</div>
+      <div>({current?.ipAddr || '--'})</div>
+      <div style={{flex: 1}} />
+      <StopStartButton />
+      <a target="_blank" style={{fontSize: 14,marginLeft: 10}} href="https://nginx.org/en/docs/">参考文档</a>
+    </div>
+    <div className="nginx-conf">
+      <Menu
+        onClick={onClick}
+        style={{ width: 256 }}
+        mode="inline"
+        items={menuItems}
+        activeKey={activeKey}
+        selectedKeys={[activeKey]}
+        onOpenChange={onOpenChange}
+        openKeys={openKeys}
+      />
+      <div className="nginx-routes">
+        {
+          !current ? (<div className="error">该实例不存在或者已被删除,<NavLink to="/">返回首页</NavLink></div> ): null
+        }
+        <Outlet />
+      </div>
+    </div>
+
+  </div>)
+}

+ 173 - 173
src/pages/nginx/list.tsx

@@ -1,173 +1,173 @@
-/**
- * @author tuonian
- * @date 2023/6/26
- */
-import {Button, Modal, Table, TableColumnsType, Tag} from 'antd'
-import {useEffect, useRef, useState} from "react";
-import NginxDemo from './nginx.json'
-import {INginx} from "../../models/nginx.ts";
-import {NginxApis} from "../../api/nginx.ts";
-import {AutoForm, AutoFormInstance, Message, Notify} from "planning-tools";
-import {useFormConfig} from "./config.tsx";
-import {DeleteOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
-import {EditNginxBtn} from "./components/EditNginxBtn.tsx";
-
-export const NginxList = ()=>{
-
-
-  const [loading,setLoading] = useState(false)
-  const [nginxList,setNginxList] = useState<INginx[]>([NginxDemo as any])
-  const [open,setOpen] = useState(false)
-  const formRef = useRef<AutoFormInstance>()
-
-  const [modal,contextHolder] = Modal.useModal()
-
-  const formConfig = useFormConfig();
-
-  const fetchData = ()=>{
-    setLoading(true)
-    NginxApis.findAll()
-      .then(({data})=>{
-        if (Array.isArray(data.data)){
-          setNginxList(data.data)
-        }
-        console.log('data',data)
-      })
-      .catch(e=>{
-        console.log('fetchData fail',e)
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-
-  }
-
-  const onAddNginx = async () =>{
-    const values = await formRef.current?.onSyncSubmit(true);
-    console.log('onAdd', values);
-    setLoading(true);
-    NginxApis.updateOrAdd(values)
-      .then(({data})=>{
-        console.log('addNginx', data)
-        Message.success('添加成功!')
-        setOpen(false)
-        fetchData()
-      })
-      .catch(e=>{
-        console.log('add fail', e)
-        if (e.code == 1){
-          fetchData()
-          Notify.warn(`实例添加成功,但环境检查失败:${e.msg || e.message}`);
-          setOpen(false)
-        }else {
-          Notify.warn(e.msg || e.message)
-        }
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
-
-  const onRemoveNginx = (data: INginx) =>{
-    modal.confirm({
-      title: '警告',
-      content: '您确认要删除该实例吗?该操作不可恢复,请谨慎操作;删除实例不会影响服务器现有的文件和状态',
-      okType: 'danger',
-      okText: '确认删除',
-      cancelText: '先不了',
-      onOk: ()=>{
-        NginxApis.delNginx(data.id)
-          .then(()=>{
-            fetchData()
-          })
-          .catch(e=>{
-            Notify.warn(e.msg || e.message)
-          })
-      }
-    })
-  }
-
-  useEffect(()=>{
-    fetchData()
-  },[])
-
-  const renderOperations = (data: INginx)=>{
-
-    return (<>
-      <EditNginxBtn nginx={data} />
-      <Button onClick={()=>onRemoveNginx(data)} danger type="text" icon={<DeleteOutlined />}/>
-    </>)
-
-  }
-
-  const columns = [
-    {
-      dataIndex: 'id',
-      title: 'ID',
-    },
-    {
-      dataIndex: 'name',
-      title:"名称"
-    },
-    {
-      dataIndex: 'isLocal',
-      title: '实例类型',
-      render: value => value ? '本地实例':<Tag color="orange">远程实例</Tag>
-    },
-    {
-      dataIndex: 'ipAddr',
-      title:'实例IP',
-      render: (value,record) => record.isLocal ? '--': value
-    },
-    {
-      dataIndex: 'remark',
-      title:'备注信息'
-    },
-    {
-      title: '操作',
-      render: (_,record)=>renderOperations(record),
-      width: 180,
-      fixed: 'right'
-    }
-
-  ] as TableColumnsType<INginx>
-
-  console.log('list' ,nginxList)
-
-  return (
-    <div className="page">
-      <div className="page-header">
-        <div>
-          Nginx实例
-        </div>
-       <div>
-         <Button loading={loading} onClick={()=>fetchData()} icon={<SyncOutlined />} />
-         <Button type="primary" loading={loading} onClick={()=>setOpen(true)}  icon={<PlusOutlined />}/>
-       </div>
-
-      </div>
-      <div className="page-container">
-        <Table
-          dataSource={nginxList}
-          columns={columns as any}
-          rowKey="id"
-        >
-        </Table>
-      </div>
-      <Modal title="新增实例"
-      open={open}
-             destroyOnClose={true}
-             maskClosable={false}
-             onCancel={()=>setOpen(false)}
-             onOk={onAddNginx}
-             confirmLoading={loading}
-             width="800px"
-      >
-        <AutoForm data={{isLocal: true}}
-                  columns={formConfig.addNginx}
-                  ref={formRef as never} />
-      </Modal>
-      {contextHolder}
-    </div>
-  )
-}
+/**
+ * @author tuonian
+ * @date 2023/6/26
+ */
+import {Button, Modal, Table, TableColumnsType, Tag} from 'antd'
+import {useEffect, useRef, useState} from "react";
+import NginxDemo from './nginx.json'
+import {INginx} from "../../models/nginx.ts";
+import {NginxApis} from "../../api/nginx.ts";
+import {AutoForm, AutoFormInstance, Message, Notify} from "planning-tools";
+import {useFormConfig} from "./config.tsx";
+import {DeleteOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
+import {EditNginxBtn} from "./components/EditNginxBtn.tsx";
+
+export const NginxList = ()=>{
+
+
+  const [loading,setLoading] = useState(false)
+  const [nginxList,setNginxList] = useState<INginx[]>([NginxDemo as any])
+  const [open,setOpen] = useState(false)
+  const formRef = useRef<AutoFormInstance>()
+
+  const [modal,contextHolder] = Modal.useModal()
+
+  const formConfig = useFormConfig();
+
+  const fetchData = ()=>{
+    setLoading(true)
+    NginxApis.findAll()
+      .then(({data})=>{
+        if (Array.isArray(data.data)){
+          setNginxList(data.data)
+        }
+        console.log('data',data)
+      })
+      .catch(e=>{
+        console.log('fetchData fail',e)
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+
+  }
+
+  const onAddNginx = async () =>{
+    const values = await formRef.current?.onSyncSubmit(true);
+    console.log('onAdd', values);
+    setLoading(true);
+    NginxApis.updateOrAdd(values)
+      .then(({data})=>{
+        console.log('addNginx', data)
+        Message.success('添加成功!')
+        setOpen(false)
+        fetchData()
+      })
+      .catch(e=>{
+        console.log('add fail', e)
+        if (e.code == 1){
+          fetchData()
+          Notify.warn(`实例添加成功,但环境检查失败:${e.msg || e.message}`);
+          setOpen(false)
+        }else {
+          Notify.warn(e.msg || e.message)
+        }
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+  }
+
+  const onRemoveNginx = (data: INginx) =>{
+    modal.confirm({
+      title: '警告',
+      content: '您确认要删除该实例吗?该操作不可恢复,请谨慎操作;删除实例不会影响服务器现有的文件和状态',
+      okType: 'danger',
+      okText: '确认删除',
+      cancelText: '先不了',
+      onOk: ()=>{
+        NginxApis.delNginx(data.id)
+          .then(()=>{
+            fetchData()
+          })
+          .catch(e=>{
+            Notify.warn(e.msg || e.message)
+          })
+      }
+    })
+  }
+
+  useEffect(()=>{
+    fetchData()
+  },[])
+
+  const renderOperations = (data: INginx)=>{
+
+    return (<>
+      <EditNginxBtn nginx={data} />
+      <Button onClick={()=>onRemoveNginx(data)} danger type="text" icon={<DeleteOutlined />}/>
+    </>)
+
+  }
+
+  const columns = [
+    {
+      dataIndex: 'id',
+      title: 'ID',
+    },
+    {
+      dataIndex: 'name',
+      title:"名称"
+    },
+    {
+      dataIndex: 'isLocal',
+      title: '实例类型',
+      render: value => value ? '本地实例':<Tag color="orange">远程实例</Tag>
+    },
+    {
+      dataIndex: 'ipAddr',
+      title:'实例IP',
+      render: (value,record) => record.isLocal ? '--': value
+    },
+    {
+      dataIndex: 'remark',
+      title:'备注信息'
+    },
+    {
+      title: '操作',
+      render: (_,record)=>renderOperations(record),
+      width: 180,
+      fixed: 'right'
+    }
+
+  ] as TableColumnsType<INginx>
+
+  console.log('list' ,nginxList)
+
+  return (
+    <div className="page">
+      <div className="page-header">
+        <div>
+          Nginx实例
+        </div>
+       <div>
+         <Button loading={loading} onClick={()=>fetchData()} icon={<SyncOutlined />} />
+         <Button type="primary" loading={loading} onClick={()=>setOpen(true)}  icon={<PlusOutlined />}/>
+       </div>
+
+      </div>
+      <div className="page-container">
+        <Table
+          dataSource={nginxList}
+          columns={columns as any}
+          rowKey="id"
+        >
+        </Table>
+      </div>
+      <Modal title="新增实例"
+      open={open}
+             destroyOnClose={true}
+             maskClosable={false}
+             onCancel={()=>setOpen(false)}
+             onOk={onAddNginx}
+             confirmLoading={loading}
+             width="800px"
+      >
+        <AutoForm data={{isLocal: true}}
+                  columns={formConfig.addNginx}
+                  ref={formRef as never} />
+      </Modal>
+      {contextHolder}
+    </div>
+  )
+}

+ 162 - 162
src/pages/nginx/location/index.tsx

@@ -1,162 +1,162 @@
-/**
- * @author tuonian
- * @date 2023/6/29
- */
-import {useNavigate, useParams} from "react-router";
-import {NginxRouteParams} from "../types.ts";
-import {useAppDispatch, useAppSelector} from "../../../store";
-import {Button, Modal, Spin} from "antd";
-import {useEffect, useRef, useState} from "react";
-import {AutoForm, AutoFormInstance, Message, uniqueKey} from "planning-tools";
-import {INginxServer, PLocation} from "../../../models/nginx.ts";
-import {useFormConfig} from "../config.tsx";
-import {NginxActions} from "../../../store/slice/nginx.ts";
-import {nginxPrefix, serverIndexRoute} from "../../../routes/routes";
-import {NavLink} from "react-router-dom";
-import {cloneDeep} from "lodash";
-import {NginxApis} from "../../../api/nginx.ts";
-
-type Props ={
-  isAdd?: boolean
-}
-export const ServerLocation = ({isAdd}: Props) => {
-
-  const nginx = useAppSelector(state => state.nginx.current)
-  const server = useAppSelector(state => state.nginx.server);
-  const [location, setLocation] = useState<PLocation>()
-  const [loading,setLoading] = useState(false)
-
-  const params = useParams<NginxRouteParams>()
-  const [modal,contextHolder] = Modal.useModal()
-
-  const formRef = useRef<AutoFormInstance>()
-
-  const formConfig = useFormConfig()
-
-  const dispatch = useAppDispatch();
-  const navigate = useNavigate();
-
-  useEffect(()=>{
-    if (!server ){
-      return
-    }
-    if (isAdd){
-      setLocation({})
-      formRef.current?.setData({id: uniqueKey(20)})
-    }else if (params.locId && params.locId !== location?.id){
-      const loc = server.locations?.find(l=>l.id === params.locId);
-      setLocation(loc)
-      formRef.current?.setData({...loc})
-      console.log('location data', loc)
-    }
-  },[server, params.locId])
-
-  const onSave =async () => {
-    if (!server || !nginx?.id){
-      return
-    }
-    const resp = await formRef.current?.onSyncSubmit(true);
-    const data = { ...location, ...resp, nginxId: nginx.id};
-    if (!data.id){
-      data.id = uniqueKey(20)
-    }
-    const postData = cloneDeep(server) as INginxServer;
-    if (!postData.locations){
-      postData.locations = []
-    }
-    if (isAdd){
-      postData.locations.push(data)
-    }else {
-      postData.locations = postData.locations.map(item=>{
-        if (item.id == data.id){
-          return { ...item, ...data}
-        }
-        return item
-      })
-    }
-   onSaveData(!!isAdd,false,postData)
-  }
-
-  const onSaveData = (isAdd: boolean, isRemove: boolean, postData: INginxServer)=>{
-    if (!nginx?.id){
-      Message.error('system error,current nginx id is zero')
-      return
-    }
-    setLoading(true);
-    NginxApis.updateServer(nginx, postData)
-      .then(()=>{
-        dispatch(NginxActions.updateServer({
-          id: postData.id,
-          locations: postData.locations
-        }))
-        if (isAdd){
-          setLocation({})
-          formRef.current?.setData({})
-        }else if (isRemove){
-          navigate(serverIndexRoute(postData.nginxId,postData.id), { replace: true } )
-        }
-        Message.success("success!")
-      })
-      .catch(e=>{
-        Message.error(e.msg || e.message)
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
-
-  const onRemove = ()=>{
-    if (isAdd || !server || !nginx){
-      return
-    }
-    modal.confirm({
-      title: '警告',
-      content: '您确定要删除该规则吗?删除之后不可恢复,请谨慎操作',
-      okText: '确认删除',
-      cancelText: '暂时不了',
-      okType: 'danger',
-      onOk: ()=>{
-        const locations = (server.locations || []).filter(item=>item.id !== location?.id);
-        const postData = {...server,locations }
-        onSaveData(false,true, postData)
-      }
-    })
-  }
-  if (!nginx){
-    return null
-  }
-  if (!server){
-    return <div className="page error">
-      该虚拟主机不存在或者已被删除,<NavLink to={nginxPrefix(nginx.id)} >返回</NavLink>
-    </div>
-  }
-
-  if (!location){
-    return <div className="page error">
-      该规则不存在或者已被删除,<NavLink to={serverIndexRoute(nginx.id, server.id)} >返回</NavLink>
-    </div>
-  }
-
-  return (<div className="page">
-    <div className="page-header">
-      {
-        isAdd ? "新增规则": "编辑规则"
-      }
-    </div>
-    <div className="page-container">
-      {
-        server && location ? (  <AutoForm
-          ref={formRef as any}
-          columns={formConfig.location}
-          data={location}
-          onlyFields={true}
-        />): <Spin />
-      }
-      <div className="page-footer">
-        <Button loading={loading} type="primary" onClick={onSave}>保存</Button>
-        <Button loading={loading} hidden={isAdd} onClick={onRemove} danger >删除</Button>
-      </div>
-    </div>
-    {contextHolder}
-  </div>)
-}
+/**
+ * @author tuonian
+ * @date 2023/6/29
+ */
+import {useNavigate, useParams} from "react-router";
+import {NginxRouteParams} from "../types.ts";
+import {useAppDispatch, useAppSelector} from "../../../store";
+import {Button, Modal, Spin} from "antd";
+import {useEffect, useRef, useState} from "react";
+import {AutoForm, AutoFormInstance, Message, uniqueKey} from "planning-tools";
+import {INginxServer, PLocation} from "../../../models/nginx.ts";
+import {useFormConfig} from "../config.tsx";
+import {NginxActions} from "../../../store/slice/nginx.ts";
+import {nginxPrefix, serverIndexRoute} from "../../../routes/routes";
+import {NavLink} from "react-router-dom";
+import {cloneDeep} from "lodash";
+import {NginxApis} from "../../../api/nginx.ts";
+
+type Props ={
+  isAdd?: boolean
+}
+export const ServerLocation = ({isAdd}: Props) => {
+
+  const nginx = useAppSelector(state => state.nginx.current)
+  const server = useAppSelector(state => state.nginx.server);
+  const [location, setLocation] = useState<PLocation>()
+  const [loading,setLoading] = useState(false)
+
+  const params = useParams<NginxRouteParams>()
+  const [modal,contextHolder] = Modal.useModal()
+
+  const formRef = useRef<AutoFormInstance>()
+
+  const formConfig = useFormConfig()
+
+  const dispatch = useAppDispatch();
+  const navigate = useNavigate();
+
+  useEffect(()=>{
+    if (!server ){
+      return
+    }
+    if (isAdd){
+      setLocation({})
+      formRef.current?.setData({id: uniqueKey(20)})
+    }else if (params.locId && params.locId !== location?.id){
+      const loc = server.locations?.find(l=>l.id === params.locId);
+      setLocation(loc)
+      formRef.current?.setData({...loc})
+      console.log('location data', loc)
+    }
+  },[server, params.locId])
+
+  const onSave =async () => {
+    if (!server || !nginx?.id){
+      return
+    }
+    const resp = await formRef.current?.onSyncSubmit(true);
+    const data = { ...location, ...resp, nginxId: nginx.id};
+    if (!data.id){
+      data.id = uniqueKey(20)
+    }
+    const postData = cloneDeep(server) as INginxServer;
+    if (!postData.locations){
+      postData.locations = []
+    }
+    if (isAdd){
+      postData.locations.push(data)
+    }else {
+      postData.locations = postData.locations.map(item=>{
+        if (item.id == data.id){
+          return { ...item, ...data}
+        }
+        return item
+      })
+    }
+   onSaveData(!!isAdd,false,postData)
+  }
+
+  const onSaveData = (isAdd: boolean, isRemove: boolean, postData: INginxServer)=>{
+    if (!nginx?.id){
+      Message.error('system error,current nginx id is zero')
+      return
+    }
+    setLoading(true);
+    NginxApis.updateServer(nginx, postData)
+      .then(()=>{
+        dispatch(NginxActions.updateServer({
+          id: postData.id,
+          locations: postData.locations
+        }))
+        if (isAdd){
+          setLocation({})
+          formRef.current?.setData({})
+        }else if (isRemove){
+          navigate(serverIndexRoute(postData.nginxId,postData.id), { replace: true } )
+        }
+        Message.success("success!")
+      })
+      .catch(e=>{
+        Message.error(e.msg || e.message)
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+  }
+
+  const onRemove = ()=>{
+    if (isAdd || !server || !nginx){
+      return
+    }
+    modal.confirm({
+      title: '警告',
+      content: '您确定要删除该规则吗?删除之后不可恢复,请谨慎操作',
+      okText: '确认删除',
+      cancelText: '暂时不了',
+      okType: 'danger',
+      onOk: ()=>{
+        const locations = (server.locations || []).filter(item=>item.id !== location?.id);
+        const postData = {...server,locations }
+        onSaveData(false,true, postData)
+      }
+    })
+  }
+  if (!nginx){
+    return null
+  }
+  if (!server){
+    return <div className="page error">
+      该虚拟主机不存在或者已被删除,<NavLink to={nginxPrefix(nginx.id)} >返回</NavLink>
+    </div>
+  }
+
+  if (!location){
+    return <div className="page error">
+      该规则不存在或者已被删除,<NavLink to={serverIndexRoute(nginx.id, server.id)} >返回</NavLink>
+    </div>
+  }
+
+  return (<div className="page">
+    <div className="page-header">
+      {
+        isAdd ? "新增规则": "编辑规则"
+      }
+    </div>
+    <div className="page-container">
+      {
+        server && location ? (  <AutoForm
+          ref={formRef as any}
+          columns={formConfig.location}
+          data={location}
+          onlyFields={true}
+        />): <Spin />
+      }
+      <div className="page-footer">
+        <Button loading={loading} type="primary" onClick={onSave}>保存</Button>
+        <Button loading={loading} hidden={isAdd} onClick={onRemove} danger >删除</Button>
+      </div>
+    </div>
+    {contextHolder}
+  </div>)
+}

+ 12 - 12
src/pages/nginx/location/new.tsx

@@ -1,12 +1,12 @@
-/**
- * @author tuonian
- * @date 2023/6/29
- */
-import {ServerLocation} from "./index.tsx";
-
-
-export const NewLocation = () => {
-
-
-  return (<ServerLocation isAdd={true} /> )
-}
+/**
+ * @author tuonian
+ * @date 2023/6/29
+ */
+import {ServerLocation} from "./index.tsx";
+
+
+export const NewLocation = () => {
+
+
+  return (<ServerLocation isAdd={true} /> )
+}

+ 75 - 75
src/pages/nginx/nginx.json

@@ -1,75 +1,75 @@
-{
-  "id": 1,
-  "servers": [
-    {
-      "id": "server1",
-      "server_name": "demo.domain.cn",
-      "listen": 80,
-      "ssl": false,
-      "charset": "koi8-r",
-      "access_log": "/var/log/nginx/host.access.log  main",
-      "locations": [
-        {
-          "id": "loc1",
-          "match": {
-            "path": "/"
-          },
-          "rewrite": {
-            "regex": "^/(.*)",
-            "replacement": "https://dev.tonyandmoney.cn/$1",
-            "flag": "permanent"
-          }
-        }
-      ]
-    },
-    {
-      "id": "server2",
-      "server_name": "demo.domain.cn",
-      "listen": 443,
-      "ssl": true,
-      "ssl_certificate": "/data/certs/demo.domain.cn.pem",
-      "ssl_certificate_key": "/data/certs/demo.domain.cn.key",
-      "ssl_session_timeout": "5m",
-      "ssl_ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
-      "ssl_protocols": ["TLSv1","TLSv1.1","TLSv1.2"],
-      "ssl_prefer_server_ciphers": "on",
-      "client_max_body_size": "500m",
-      "locations": [
-
-        {
-          "id":"loc2",
-          "match": {
-            "path": "/common/"
-          },
-          "index": "index.html",
-          "proxy_set_header": [
-            {
-              "name": "Host",
-              "value": "$host"
-            },
-            {
-              "name": "X-Real-IP",
-              "value": "$remote_addr"
-            },
-            {
-              "name": "X-Forwarded-For",
-              "value": "$proxy_add_x_forwarded_for"
-            },
-            {
-              "name": "Upgrade",
-              "value": "$http_upgrade"
-            },
-            {
-              "name": "Connection",
-              "value": "upgrade"
-            }
-          ],
-          "proxy_pass": "http://172.16.0.5:3006/common/",
-          "proxy_next_upstream": ["http_502","http_504","http_404","error","timeout","invalid_header"],
-          "proxy_connect_timeout": "60s",
-          "proxy_http_version": "1.1"
-        }
-      ]
-    }
-  ]
-}
+{
+  "id": 1,
+  "servers": [
+    {
+      "id": "server1",
+      "server_name": "demo.domain.cn",
+      "listen": 80,
+      "ssl": false,
+      "charset": "koi8-r",
+      "access_log": "/var/log/nginx/host.access.log  main",
+      "locations": [
+        {
+          "id": "loc1",
+          "match": {
+            "path": "/"
+          },
+          "rewrite": {
+            "regex": "^/(.*)",
+            "replacement": "https://dev.tonyandmoney.cn/$1",
+            "flag": "permanent"
+          }
+        }
+      ]
+    },
+    {
+      "id": "server2",
+      "server_name": "demo.domain.cn",
+      "listen": 443,
+      "ssl": true,
+      "ssl_certificate": "/data/certs/demo.domain.cn.pem",
+      "ssl_certificate_key": "/data/certs/demo.domain.cn.key",
+      "ssl_session_timeout": "5m",
+      "ssl_ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4",
+      "ssl_protocols": ["TLSv1","TLSv1.1","TLSv1.2"],
+      "ssl_prefer_server_ciphers": "on",
+      "client_max_body_size": "500m",
+      "locations": [
+
+        {
+          "id":"loc2",
+          "match": {
+            "path": "/common/"
+          },
+          "index": "index.html",
+          "proxy_set_header": [
+            {
+              "name": "Host",
+              "value": "$host"
+            },
+            {
+              "name": "X-Real-IP",
+              "value": "$remote_addr"
+            },
+            {
+              "name": "X-Forwarded-For",
+              "value": "$proxy_add_x_forwarded_for"
+            },
+            {
+              "name": "Upgrade",
+              "value": "$http_upgrade"
+            },
+            {
+              "name": "Connection",
+              "value": "upgrade"
+            }
+          ],
+          "proxy_pass": "http://172.16.0.5:3006/common/",
+          "proxy_next_upstream": ["http_502","http_504","http_404","error","timeout","invalid_header"],
+          "proxy_connect_timeout": "60s",
+          "proxy_http_version": "1.1"
+        }
+      ]
+    }
+  ]
+}

+ 70 - 70
src/pages/nginx/server/components/SyncButton.tsx

@@ -1,70 +1,70 @@
-import {Button, Tooltip} from "antd";
-import {useState} from "react";
-import {useAppDispatch, useAppSelector} from "../../../../store";
-import {NginxApis} from "../../../../api/nginx.ts";
-import {Message} from "planning-tools";
-import {QuestionCircleOutlined} from "@ant-design/icons";
-import {createServerHost} from "../../utils/nginx.ts";
-import {NginxActions} from "../../../../store/slice/nginx.ts";
-import {INginxServer} from "../../../../models/nginx.ts";
-
-type IProps = {
-  onSubmitData: ()=>Promise<false | INginxServer>
-  upstream?: boolean
-}
-/**
- * 将server的配置同步到服务器
- * @constructor
- */
-export const SyncButton = ({ onSubmitData, upstream}: IProps) => {
-
-    const [loading,setLoading] = useState(false)
-    const nginx = useAppSelector(state => state.nginx.current);
-
-    const dispatch = useAppDispatch()
-    /**
-     * 将配置文件同步到服务器
-     */
-    const onSyncServer = async () => {
-        if (!nginx?.id){
-            return
-        }
-      const serverData = await onSubmitData();
-        if (!serverData){
-          return
-        }
-        setLoading(true);
-        const postData = createServerHost(nginx,serverData)
-        NginxApis.refreshServer(postData)
-            .then(()=>{
-                Message.success("sync success!");
-                const updateData: Partial<INginxServer>={
-                  ...serverData,
-                  confData: postData.serverConf,
-                }
-                if (upstream){
-                  dispatch(NginxActions.updateUpstream(updateData))
-                }else {
-                  dispatch(NginxActions.updateServer(updateData))
-                }
-            })
-            .finally(()=>{
-                setLoading(false)
-            })
-    }
-
-    if (!nginx?.id){
-        return null
-    }
-    return (
-        <>
-            <Button danger loading={loading} onClick={onSyncServer}>
-                同步
-                <Tooltip placement="left" title="同步配置文件到服务器,如果该server为禁用状态,将从服务器删除该配置文件">
-                    <QuestionCircleOutlined />
-                </Tooltip>
-            </Button>
-        </>
-
-    )
-}
+import {Button, Tooltip} from "antd";
+import {useState} from "react";
+import {useAppDispatch, useAppSelector} from "../../../../store";
+import {NginxApis} from "../../../../api/nginx.ts";
+import {Message} from "planning-tools";
+import {QuestionCircleOutlined} from "@ant-design/icons";
+import {createServerHost} from "../../utils/nginx.ts";
+import {NginxActions} from "../../../../store/slice/nginx.ts";
+import {INginxServer} from "../../../../models/nginx.ts";
+
+type IProps = {
+  onSubmitData: ()=>Promise<false | INginxServer>
+  upstream?: boolean
+}
+/**
+ * 将server的配置同步到服务器
+ * @constructor
+ */
+export const SyncButton = ({ onSubmitData, upstream}: IProps) => {
+
+    const [loading,setLoading] = useState(false)
+    const nginx = useAppSelector(state => state.nginx.current);
+
+    const dispatch = useAppDispatch()
+    /**
+     * 将配置文件同步到服务器
+     */
+    const onSyncServer = async () => {
+        if (!nginx?.id){
+            return
+        }
+      const serverData = await onSubmitData();
+        if (!serverData){
+          return
+        }
+        setLoading(true);
+        const postData = createServerHost(nginx,serverData)
+        NginxApis.refreshServer(postData)
+            .then(()=>{
+                Message.success("sync success!");
+                const updateData: Partial<INginxServer>={
+                  ...serverData,
+                  confData: postData.serverConf,
+                }
+                if (upstream){
+                  dispatch(NginxActions.updateUpstream(updateData))
+                }else {
+                  dispatch(NginxActions.updateServer(updateData))
+                }
+            })
+            .finally(()=>{
+                setLoading(false)
+            })
+    }
+
+    if (!nginx?.id){
+        return null
+    }
+    return (
+        <>
+            <Button danger loading={loading} onClick={onSyncServer}>
+                同步
+                <Tooltip placement="left" title="同步配置文件到服务器,如果该server为禁用状态,将从服务器删除该配置文件">
+                    <QuestionCircleOutlined />
+                </Tooltip>
+            </Button>
+        </>
+
+    )
+}

+ 28 - 28
src/pages/nginx/server/components/preview.less

@@ -1,28 +1,28 @@
-.preview-pane{
-  min-height: 450px;
-  .ant-input,.ant-input[disabled]{
-    color: #333;
-    background-color: transparent;
-    cursor: text;
-    min-height: 450px;
-  }
-
-  .ops{
-    margin-top: 10px;
-    text-align: right;
-    padding-right: 20px;
-    .ant-btn.ant-btn{
-      margin-left: 10px;
-    }
-  }
-}
-
-.preview-modal{
-  .ant-modal-body{
-    padding: 10px;
-  }
-
-  .ant-alert-with-description.ant-alert-no-icon{
-    padding: 5px 10px;
-  }
-}
+.preview-pane{
+  min-height: 450px;
+  .ant-input,.ant-input[disabled]{
+    color: #333;
+    background-color: transparent;
+    cursor: text;
+    min-height: 450px;
+  }
+
+  .ops{
+    margin-top: 10px;
+    text-align: right;
+    padding-right: 20px;
+    .ant-btn.ant-btn{
+      margin-left: 10px;
+    }
+  }
+}
+
+.preview-modal{
+  .ant-modal-body{
+    padding: 10px;
+  }
+
+  .ant-alert-with-description.ant-alert-no-icon{
+    padding: 5px 10px;
+  }
+}

+ 115 - 115
src/pages/nginx/server/components/preview.tsx

@@ -1,115 +1,115 @@
-/**
- * @author tuonian
- * @date 2023/6/28
- */
-import {INginxServer} from "../../../../models/nginx.ts";
-import {Alert, Button, Input, Modal} from "antd";
-import {useEffect, useState} from "react";
-import {renderServer} from "../../utils";
-import {Message, Notify} from "planning-tools";
-
-import './preview.less'
-import {useAppSelector} from "../../../../store";
-import {createServerHost} from "../../utils/nginx.ts";
-import {NginxApis} from "../../../../api/nginx.ts";
-
-
-type Props = {
-  modal?: boolean
-  onGetData?: () => Promise<Partial<INginxServer>>
-  server?: INginxServer
-}
-/**
- * server的预览+编辑
- * @param modal
- * @param server
- * @param onGetData
- * @constructor
- */
-export const ServerPreviewConf = ({modal, server, onGetData}: Props) => {
-
-  const [loading,setLoading] = useState(false)
-  const [data,setData] = useState<Partial<INginxServer>>()
-  const nginx = useAppSelector(state => state.nginx.current)
-
-  const [value,setValue] = useState<string>()
-
-  const openPreview = async ()=>{
-    setLoading(true);
-    try {
-      const resp = await onGetData?.() || server
-      setData(resp)
-    }catch (e){
-      console.log('[preview] fail',e)
-      Notify.warn(`获取数据失败!`)
-    }finally {
-      setLoading(false)
-    }
-  }
-
-  useEffect(()=>{
-    if (!modal){
-      setData(server)
-    }
-  },[server])
-
-  const onRefresh = ()=>{
-    if (!nginx?.id || !server?.id){
-      return
-    }
-    const postData = createServerHost(nginx,server);
-    if (value){
-      postData.serverConf = value
-    }
-    setLoading(true)
-    NginxApis.refreshServer(postData)
-      .then(()=>{
-        Message.success('sync success!')
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
-
-  useEffect(()=>{
-    if (!nginx?.id || !data){
-      setValue(undefined)
-    }else {
-      const conf = renderServer(nginx,data)
-      setValue(conf)
-    }
-
-  },[data, nginx])
-
-  const renderConf = () => {
-    return (<div className="preview-pane">
-      <Alert type="warning" style={{marginBottom: 5}}
-             closable={true}
-             description={`在此处对配置文件的修改,将在虚拟主机通过界面操作"同步"功能后丢失,请注意`}></Alert>
-      <Input.TextArea onChange={v=>setValue(v.currentTarget.value)} value={value} />
-      <div className="ops">
-        <Button onClick={onRefresh} danger type="primary">同步</Button>
-        <Button onClick={()=>setData(undefined)}>取消</Button>
-      </div>
-    </div>)
-  }
-
-  if (modal){
-    return  <>
-      <Button loading={loading} onClick={()=>openPreview()}>配置文件</Button>
-      <Modal
-        open={!!data}
-        footer={null}
-        width="900px"
-        className="preview-modal"
-        onCancel={()=>setData(undefined)}
-        maskClosable={false}
-        title="配置文件">
-        {renderConf()}
-      </Modal>
-    </>
-  }
-
-  return renderConf()
-
-}
+/**
+ * @author tuonian
+ * @date 2023/6/28
+ */
+import {INginxServer} from "../../../../models/nginx.ts";
+import {Alert, Button, Input, Modal} from "antd";
+import {useEffect, useState} from "react";
+import {renderServer} from "../../utils";
+import {Message, Notify} from "planning-tools";
+
+import './preview.less'
+import {useAppSelector} from "../../../../store";
+import {createServerHost} from "../../utils/nginx.ts";
+import {NginxApis} from "../../../../api/nginx.ts";
+
+
+type Props = {
+  modal?: boolean
+  onGetData?: () => Promise<Partial<INginxServer>>
+  server?: INginxServer
+}
+/**
+ * server的预览+编辑
+ * @param modal
+ * @param server
+ * @param onGetData
+ * @constructor
+ */
+export const ServerPreviewConf = ({modal, server, onGetData}: Props) => {
+
+  const [loading,setLoading] = useState(false)
+  const [data,setData] = useState<Partial<INginxServer>>()
+  const nginx = useAppSelector(state => state.nginx.current)
+
+  const [value,setValue] = useState<string>()
+
+  const openPreview = async ()=>{
+    setLoading(true);
+    try {
+      const resp = await onGetData?.() || server
+      setData(resp)
+    }catch (e){
+      console.log('[preview] fail',e)
+      Notify.warn(`获取数据失败!`)
+    }finally {
+      setLoading(false)
+    }
+  }
+
+  useEffect(()=>{
+    if (!modal){
+      setData(server)
+    }
+  },[server])
+
+  const onRefresh = ()=>{
+    if (!nginx?.id || !server?.id){
+      return
+    }
+    const postData = createServerHost(nginx,server);
+    if (value){
+      postData.serverConf = value
+    }
+    setLoading(true)
+    NginxApis.refreshServer(postData)
+      .then(()=>{
+        Message.success('sync success!')
+      })
+      .finally(()=>{
+        setLoading(false)
+      })
+  }
+
+  useEffect(()=>{
+    if (!nginx?.id || !data){
+      setValue(undefined)
+    }else {
+      const conf = renderServer(nginx,data)
+      setValue(conf)
+    }
+
+  },[data, nginx])
+
+  const renderConf = () => {
+    return (<div className="preview-pane">
+      <Alert type="warning" style={{marginBottom: 5}}
+             closable={true}
+             description={`在此处对配置文件的修改,将在虚拟主机通过界面操作"同步"功能后丢失,请注意`}></Alert>
+      <Input.TextArea onChange={v=>setValue(v.currentTarget.value)} value={value} />
+      <div className="ops">
+        <Button onClick={onRefresh} danger type="primary">同步</Button>
+        <Button onClick={()=>setData(undefined)}>取消</Button>
+      </div>
+    </div>)
+  }
+
+  if (modal){
+    return  <>
+      <Button loading={loading} onClick={()=>openPreview()}>配置文件</Button>
+      <Modal
+        open={!!data}
+        footer={null}
+        width="900px"
+        className="preview-modal"
+        onCancel={()=>setData(undefined)}
+        maskClosable={false}
+        title="配置文件">
+        {renderConf()}
+      </Modal>
+    </>
+  }
+
+  return renderConf()
+
+}

Some files were not shown because too many files changed in this diff