1
0
Эх сурвалжийг харах

[v1.1] 新增自动续签并修复了一些 bug

修复后端可能会 panic 的问题
修复前端逻辑问题,新增证书自动续签
0xJacky 3 жил өмнө
parent
commit
882fe8c074
100 өөрчлөгдсөн 721 нэмэгдсэн , 246 устгасан
  1. 3 2
      README.md
  2. 2 0
      frontend/babel.config.js
  3. BIN
      frontend/dist/favicon.ico
  4. BIN
      frontend/dist/img/logo.9e691c6b.png
  5. 0 0
      frontend/dist/index.html
  6. 0 0
      frontend/dist/js/chunk-006e4cbc-legacy.ce4defda.js
  7. 0 0
      frontend/dist/js/chunk-006e4cbc.ce4defda.js
  8. 0 0
      frontend/dist/js/chunk-00be396e-legacy.f6afc813.js
  9. 0 0
      frontend/dist/js/chunk-00be396e.f6afc813.js
  10. 1 0
      frontend/dist/js/chunk-0393876a-legacy.0e2e8183.js
  11. 1 0
      frontend/dist/js/chunk-0393876a.0e2e8183.js
  12. 0 0
      frontend/dist/js/chunk-05148b16-legacy.66291bd9.js
  13. 0 0
      frontend/dist/js/chunk-05148b16.66291bd9.js
  14. 1 0
      frontend/dist/js/chunk-09f0acda-legacy.b788b1ae.js
  15. 1 0
      frontend/dist/js/chunk-09f0acda.b788b1ae.js
  16. 0 0
      frontend/dist/js/chunk-1188de6e-legacy.f8e092e0.js
  17. 0 0
      frontend/dist/js/chunk-1188de6e.f8e092e0.js
  18. 0 0
      frontend/dist/js/chunk-1e5147a5-legacy.3620bb79.js
  19. 0 0
      frontend/dist/js/chunk-1e5147a5.3620bb79.js
  20. 0 1
      frontend/dist/js/chunk-23e3da44-legacy.bae4ce87.js
  21. 0 1
      frontend/dist/js/chunk-23e3da44.bae4ce87.js
  22. 0 0
      frontend/dist/js/chunk-2881409a-legacy.1f853726.js
  23. 0 0
      frontend/dist/js/chunk-2881409a.1f853726.js
  24. 0 0
      frontend/dist/js/chunk-2b94df79-legacy.2bf29671.js
  25. 0 0
      frontend/dist/js/chunk-2b94df79.2bf29671.js
  26. 0 0
      frontend/dist/js/chunk-2d0cf277-legacy.c260d8d5.js
  27. 0 0
      frontend/dist/js/chunk-2d0cf277.c260d8d5.js
  28. 0 0
      frontend/dist/js/chunk-312c57da-legacy.53fa07de.js
  29. 0 0
      frontend/dist/js/chunk-312c57da.53fa07de.js
  30. 0 0
      frontend/dist/js/chunk-4216c952-legacy.f0073bdb.js
  31. 0 0
      frontend/dist/js/chunk-4216c952.f0073bdb.js
  32. 0 0
      frontend/dist/js/chunk-46dcb584-legacy.5ee0f4ea.js
  33. 0 0
      frontend/dist/js/chunk-46dcb584.5ee0f4ea.js
  34. 0 0
      frontend/dist/js/chunk-4f82bf3d-legacy.8d3be338.js
  35. 0 0
      frontend/dist/js/chunk-4f82bf3d.8d3be338.js
  36. 0 0
      frontend/dist/js/chunk-5573b71a-legacy.92b99af4.js
  37. 0 0
      frontend/dist/js/chunk-5573b71a.92b99af4.js
  38. 0 0
      frontend/dist/js/chunk-59b694c3-legacy.ef41aa76.js
  39. 0 0
      frontend/dist/js/chunk-59b694c3.ef41aa76.js
  40. 1 1
      frontend/dist/js/chunk-5d4d188e-legacy.b0ffa164.js
  41. 1 1
      frontend/dist/js/chunk-5d4d188e.b0ffa164.js
  42. 0 0
      frontend/dist/js/chunk-5f8bd6de-legacy.b088830d.js
  43. 0 0
      frontend/dist/js/chunk-5f8bd6de.b088830d.js
  44. 0 1
      frontend/dist/js/chunk-680936db-legacy.547a4157.js
  45. 0 1
      frontend/dist/js/chunk-680936db.547a4157.js
  46. 0 0
      frontend/dist/js/chunk-6a4ca29d-legacy.5593b7e1.js
  47. 0 0
      frontend/dist/js/chunk-6a4ca29d.5593b7e1.js
  48. 0 0
      frontend/dist/js/chunk-7723cf62-legacy.c3f452ca.js
  49. 0 0
      frontend/dist/js/chunk-7723cf62.c3f452ca.js
  50. 0 0
      frontend/dist/js/chunk-83d83096-legacy.72980dc3.js
  51. 0 0
      frontend/dist/js/chunk-83d83096.72980dc3.js
  52. 0 1
      frontend/dist/js/chunk-96068e84-legacy.05f16d69.js
  53. 0 1
      frontend/dist/js/chunk-96068e84.05f16d69.js
  54. 0 0
      frontend/dist/js/chunk-b508de6a-legacy.b421b1eb.js
  55. 0 0
      frontend/dist/js/chunk-b508de6a.b421b1eb.js
  56. 0 0
      frontend/dist/js/chunk-c8c0a686-legacy.85e5c7a1.js
  57. 0 0
      frontend/dist/js/chunk-c8c0a686.85e5c7a1.js
  58. 0 0
      frontend/dist/js/chunk-ddbf168e-legacy.82ea029a.js
  59. 0 0
      frontend/dist/js/chunk-ddbf168e.82ea029a.js
  60. 0 0
      frontend/dist/js/chunk-e0ad5fdc-legacy.44fe5a47.js
  61. 0 0
      frontend/dist/js/chunk-e0ad5fdc.44fe5a47.js
  62. 0 0
      frontend/dist/js/chunk-vendors-legacy.731e48fc.js
  63. 0 0
      frontend/dist/js/chunk-vendors.731e48fc.js
  64. 0 0
      frontend/dist/js/index-legacy.e8aba462.js
  65. 0 0
      frontend/dist/js/index-legacy.ec1dfd27.js
  66. 0 0
      frontend/dist/js/index.62a46eff.js
  67. 0 0
      frontend/dist/js/index.fa8b2c24.js
  68. 1 1
      frontend/dist/version.json
  69. 2 1
      frontend/package.json
  70. BIN
      frontend/public/favicon.ico
  71. 8 0
      frontend/src/api/domain.js
  72. BIN
      frontend/src/assets/img/logo.png
  73. 8 2
      frontend/src/components/Logo/Logo.vue
  74. 53 0
      frontend/src/components/StdDataEntry/StdCheckGroup.vue
  75. 7 3
      frontend/src/components/StdDataEntry/StdCheckTag.vue
  76. 57 12
      frontend/src/components/StdDataEntry/StdDataEntry.vue
  77. 75 0
      frontend/src/components/StdDataEntry/StdMultiCheckTag.vue
  78. 32 8
      frontend/src/components/StdDataEntry/StdMultiFilesUpload.vue
  79. 49 0
      frontend/src/components/StdDataEntry/StdRadioGroup.vue
  80. 52 12
      frontend/src/components/StdDataEntry/StdSelector.vue
  81. 30 8
      frontend/src/components/StdDataEntry/StdSingleFileUpload.vue
  82. 13 7
      frontend/src/components/StdDataEntry/StdUpload.vue
  83. 11 11
      frontend/src/router/index.js
  84. 0 119
      frontend/src/views/Login.vue
  85. 0 0
      frontend/src/views/config/Config.vue
  86. 0 0
      frontend/src/views/config/ConfigEdit.vue
  87. 0 0
      frontend/src/views/doashboard/DashBoard.vue
  88. 1 1
      frontend/src/views/domain/CertInfo.vue
  89. 65 0
      frontend/src/views/domain/DomainAdd.vue
  90. 66 47
      frontend/src/views/domain/DomainEdit.vue
  91. 1 1
      frontend/src/views/domain/DomainList.vue
  92. 13 1
      frontend/src/views/domain/columns.js
  93. 29 0
      frontend/src/views/domain/methods.js
  94. 11 1
      frontend/src/views/other/About.vue
  95. 0 0
      frontend/src/views/other/Error.vue
  96. 0 0
      frontend/src/views/other/Home.vue
  97. 0 0
      frontend/src/views/other/Install.vue
  98. 125 0
      frontend/src/views/other/Login.vue
  99. 0 0
      frontend/src/views/user/User.vue
  100. 1 1
      frontend/version.json

+ 3 - 2
README.md

@@ -1,13 +1,14 @@
 # Nginx UI
 Yet another Nginx Web UI
 
-Version: 1.0.0
+Version: 1.1.0
 
 ## 项目特色
 
 1. 可在线查看服务器 CPU、内存、load average、磁盘使用率等指标
 2. 可一键申请 Let's encrypt 证书
-3. 在线编辑网站配置文件
+3. 可自动续签 Let's encrypt 证书
+4. 在线编辑网站配置文件
 
 ## 项目预览
 

+ 2 - 0
frontend/babel.config.js

@@ -3,6 +3,8 @@ module.exports = {
         "@vue/cli-plugin-babel/preset"
     ],
     "plugins": [
+        '@babel/plugin-proposal-optional-chaining',
+        '@babel/plugin-proposal-nullish-coalescing-operator',
         ["import", {"libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true}, "syntax-dynamic-import"]
     ],
 }

BIN
frontend/dist/favicon.ico


BIN
frontend/dist/img/logo.9e691c6b.png


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/index.html


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-006e4cbc-legacy.ce4defda.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-006e4cbc.ce4defda.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-00be396e-legacy.f6afc813.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-00be396e.f6afc813.js


+ 1 - 0
frontend/dist/js/chunk-0393876a-legacy.0e2e8183.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0393876a"],{"0efa":function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,f=(e("8f3d"),e("2877")),u=Object(f["a"])(r,a,o,!1,null,"fe43c41a",null);n["default"]=u.exports},1175:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-fe43c41a]{margin:10px}@media (max-width:512px){.ant-card[data-v-fe43c41a]{margin:10px 0}}",""]),t.exports=n},"48b1":function(t,n,e){var a=e("1175");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("77d6a146",a,!0,{sourceMap:!1,shadowMode:!1})},"8f3d":function(t,n,e){"use strict";e("48b1")}}]);

+ 1 - 0
frontend/dist/js/chunk-0393876a.0e2e8183.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0393876a"],{"0efa":function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,f=(e("8f3d"),e("2877")),u=Object(f["a"])(r,a,o,!1,null,"fe43c41a",null);n["default"]=u.exports},1175:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-fe43c41a]{margin:10px}@media (max-width:512px){.ant-card[data-v-fe43c41a]{margin:10px 0}}",""]),t.exports=n},"48b1":function(t,n,e){var a=e("1175");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("77d6a146",a,!0,{sourceMap:!1,shadowMode:!1})},"8f3d":function(t,n,e){"use strict";e("48b1")}}]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-05148b16-legacy.66291bd9.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-05148b16.66291bd9.js


+ 1 - 0
frontend/dist/js/chunk-09f0acda-legacy.b788b1ae.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-09f0acda"],{"0561":function(t,a,e){var o=e("504c");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("4267656c",o,!0,{sourceMap:!1,shadowMode:!1})},"1f35":function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],c={name:"Error"},r=c,n=(e("629f"),e("2877")),s=Object(n["a"])(r,o,i,!1,null,"0b28b6c0",null);a["default"]=s.exports},"504c":function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-0b28b6c0],div[data-v-0b28b6c0],h1[data-v-0b28b6c0],html[data-v-0b28b6c0]{padding:0;margin:0}body[data-v-0b28b6c0],html[data-v-0b28b6c0]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-0b28b6c0]{font-size:8em;font-weight:100}a[data-v-0b28b6c0]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-0b28b6c0]:active,a[data-v-0b28b6c0]:hover{color:#5bb0ed}.wrapper[data-v-0b28b6c0]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a},"629f":function(t,a,e){"use strict";e("0561")}}]);

+ 1 - 0
frontend/dist/js/chunk-09f0acda.b788b1ae.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-09f0acda"],{"0561":function(t,a,e){var o=e("504c");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("4267656c",o,!0,{sourceMap:!1,shadowMode:!1})},"1f35":function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],c={name:"Error"},r=c,n=(e("629f"),e("2877")),s=Object(n["a"])(r,o,i,!1,null,"0b28b6c0",null);a["default"]=s.exports},"504c":function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-0b28b6c0],div[data-v-0b28b6c0],h1[data-v-0b28b6c0],html[data-v-0b28b6c0]{padding:0;margin:0}body[data-v-0b28b6c0],html[data-v-0b28b6c0]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-0b28b6c0]{font-size:8em;font-weight:100}a[data-v-0b28b6c0]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-0b28b6c0]:active,a[data-v-0b28b6c0]:hover{color:#5bb0ed}.wrapper[data-v-0b28b6c0]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a},"629f":function(t,a,e){"use strict";e("0561")}}]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-1188de6e-legacy.f8e092e0.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-1188de6e.f8e092e0.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-1e5147a5-legacy.3620bb79.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-1e5147a5.3620bb79.js


+ 0 - 1
frontend/dist/js/chunk-23e3da44-legacy.bae4ce87.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-23e3da44"],{b2cd:function(t,a,e){"use strict";e("e49f")},dda8:function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],r={name:"Error"},c=r,n=(e("b2cd"),e("2877")),s=Object(n["a"])(c,o,i,!1,null,"c0697242",null);a["default"]=s.exports},e49f:function(t,a,e){var o=e("fdbe");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("6f232624",o,!0,{sourceMap:!1,shadowMode:!1})},fdbe:function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-c0697242],div[data-v-c0697242],h1[data-v-c0697242],html[data-v-c0697242]{padding:0;margin:0}body[data-v-c0697242],html[data-v-c0697242]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-c0697242]{font-size:8em;font-weight:100}a[data-v-c0697242]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-c0697242]:active,a[data-v-c0697242]:hover{color:#5bb0ed}.wrapper[data-v-c0697242]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a}}]);

+ 0 - 1
frontend/dist/js/chunk-23e3da44.bae4ce87.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-23e3da44"],{b2cd:function(t,a,e){"use strict";e("e49f")},dda8:function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],r={name:"Error"},c=r,n=(e("b2cd"),e("2877")),s=Object(n["a"])(c,o,i,!1,null,"c0697242",null);a["default"]=s.exports},e49f:function(t,a,e){var o=e("fdbe");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("6f232624",o,!0,{sourceMap:!1,shadowMode:!1})},fdbe:function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-c0697242],div[data-v-c0697242],h1[data-v-c0697242],html[data-v-c0697242]{padding:0;margin:0}body[data-v-c0697242],html[data-v-c0697242]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-c0697242]{font-size:8em;font-weight:100}a[data-v-c0697242]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-c0697242]:active,a[data-v-c0697242]:hover{color:#5bb0ed}.wrapper[data-v-c0697242]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a}}]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-2881409a-legacy.1f853726.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-2881409a.1f853726.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-2b94df79-legacy.2bf29671.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-2b94df79.2bf29671.js


+ 0 - 0
frontend/dist/js/chunk-2d0cf277-legacy.c3db42df.js → frontend/dist/js/chunk-2d0cf277-legacy.c260d8d5.js


+ 0 - 0
frontend/dist/js/chunk-2d0cf277.c3db42df.js → frontend/dist/js/chunk-2d0cf277.c260d8d5.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-312c57da-legacy.53fa07de.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-312c57da.53fa07de.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-4216c952-legacy.f0073bdb.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-4216c952.f0073bdb.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-46dcb584-legacy.5ee0f4ea.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-46dcb584.5ee0f4ea.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-4f82bf3d-legacy.8d3be338.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-4f82bf3d.8d3be338.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-5573b71a-legacy.92b99af4.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-5573b71a.92b99af4.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-59b694c3-legacy.ef41aa76.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-59b694c3.ef41aa76.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 1
frontend/dist/js/chunk-5d4d188e-legacy.b0ffa164.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 1
frontend/dist/js/chunk-5d4d188e.b0ffa164.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-5f8bd6de-legacy.b088830d.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-5f8bd6de.b088830d.js


+ 0 - 1
frontend/dist/js/chunk-680936db-legacy.547a4157.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-680936db"],{"37d1":function(t,n,e){"use strict";e("939c")},"939c":function(t,n,e){var a=e("e78d");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("29ba208a",a,!0,{sourceMap:!1,shadowMode:!1})},e33d:function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,u=(e("37d1"),e("2877")),f=Object(u["a"])(r,a,o,!1,null,"4076690a",null);n["default"]=f.exports},e78d:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-4076690a]{margin:10px}@media (max-width:512px){.ant-card[data-v-4076690a]{margin:10px 0}}",""]),t.exports=n}}]);

+ 0 - 1
frontend/dist/js/chunk-680936db.547a4157.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-680936db"],{"37d1":function(t,n,e){"use strict";e("939c")},"939c":function(t,n,e){var a=e("e78d");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("29ba208a",a,!0,{sourceMap:!1,shadowMode:!1})},e33d:function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,u=(e("37d1"),e("2877")),f=Object(u["a"])(r,a,o,!1,null,"4076690a",null);n["default"]=f.exports},e78d:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-4076690a]{margin:10px}@media (max-width:512px){.ant-card[data-v-4076690a]{margin:10px 0}}",""]),t.exports=n}}]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-6a4ca29d-legacy.5593b7e1.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-6a4ca29d.5593b7e1.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-7723cf62-legacy.c3f452ca.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-7723cf62.c3f452ca.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-83d83096-legacy.72980dc3.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-83d83096.72980dc3.js


+ 0 - 1
frontend/dist/js/chunk-96068e84-legacy.05f16d69.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="16")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);

+ 0 - 1
frontend/dist/js/chunk-96068e84.05f16d69.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="16")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-b508de6a-legacy.b421b1eb.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-b508de6a.b421b1eb.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-c8c0a686-legacy.85e5c7a1.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-c8c0a686.85e5c7a1.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-ddbf168e-legacy.82ea029a.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-ddbf168e.82ea029a.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-e0ad5fdc-legacy.44fe5a47.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-e0ad5fdc.44fe5a47.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-vendors-legacy.731e48fc.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/chunk-vendors.731e48fc.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/index-legacy.e8aba462.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/index-legacy.ec1dfd27.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/index.62a46eff.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
frontend/dist/js/index.fa8b2c24.js


+ 1 - 1
frontend/dist/version.json

@@ -1 +1 @@
-{"version":"1.0.0","build_id":12,"total_build":16}
+{"version":"1.1.0","build_id":2,"total_build":19}

+ 2 - 1
frontend/package.json

@@ -1,6 +1,6 @@
 {
     "name": "nginx-ui-frontend",
-    "version": "1.0.0",
+    "version": "1.1.0",
     "private": true,
     "scripts": {
         "serve": "vue-cli-service serve",
@@ -51,6 +51,7 @@
         "sass-loader": "^10",
         "vue-cli-plugin-generate-build-id": "^0.2.0",
         "vue-cropper": "^0.5.6",
+        "vue-template-babel-compiler": "^1.1.2",
         "vue-template-compiler": "^2.6.11"
     },
     "eslintConfig": {

BIN
frontend/public/favicon.ico


+ 8 - 0
frontend/src/api/domain.js

@@ -33,6 +33,14 @@ const domain = {
 
     cert_info(domain) {
         return http.get('cert/' + domain + '/info')
+    },
+
+    add_auto_cert(domain) {
+        return http.post('cert/' + domain)
+    },
+
+    remove_auto_cert(domain) {
+        return http.delete('cert/' + domain)
     }
 }
 

BIN
frontend/src/assets/img/logo.png


+ 8 - 2
frontend/src/components/Logo/Logo.vue

@@ -1,5 +1,6 @@
 <template>
     <div class="logo">
+        <img :src="logo" alt="logo" />
         <p class="text">Nginx UI</p>
         <div class="clear"></div>
     </div>
@@ -7,7 +8,12 @@
 
 <script>
 export default {
-    name: 'Logo'
+    name: 'Logo',
+    data() {
+        return {
+            logo: require('@/assets/img/logo.png')
+        }
+    }
 }
 </script>
 
@@ -31,7 +37,7 @@ export default {
         font-size: 23px;
         line-height: 48px;
         height: 48px;
-        text-align: center;
+        padding-left: 52px;
     }
 }
 </style>

+ 53 - 0
frontend/src/components/StdDataEntry/StdCheckGroup.vue

@@ -0,0 +1,53 @@
+<template>
+    <div>
+        <a-checkbox-group v-model="checkedList" :options="options" @change="onChange"/>
+        <template v-if="allowOther&&checkedList.indexOf('其他')>0">
+            <a-form-item label="其他">
+                <a-input v-model="other" @change="onChangeOther"/>
+            </a-form-item>
+        </template>
+    </div>
+</template>
+<script>
+export default {
+    name: 'StdCheckGroup',
+    props: {
+        options: Array,
+        allowOther: Boolean,
+        data: {
+            type: Object,
+            default() {
+                return {
+                    checkedList: [],
+                    other: ''
+                }
+            }
+        }
+    },
+    model: {
+        prop: 'data',
+        event: 'changeData'
+    },
+    watch: {
+        data() {
+            this.checkedList = this.data.checkedList
+            this.other = this.data.other
+        }
+    },
+    data() {
+        return {
+            checkedList: this.data.checkedList,
+            other: this.data.other
+        }
+    },
+    methods: {
+        onChange(checkedList) {
+            this.checkedList = checkedList
+            this.$emit('changeData', this.$data)
+        },
+        onChangeOther() {
+            this.$emit('changeData', this.$data)
+        }
+    },
+}
+</script>

+ 7 - 3
frontend/src/components/StdDataEntry/StdCheckTag.vue

@@ -14,13 +14,14 @@
 
 <script>
 export default {
-    name: "StdCheckTag",
+    name: 'StdCheckTag',
     data() {
         return {
             selectedTag: '',
         }
     },
     props: {
+        disabled: [Boolean],
         value: [Number, String, Boolean],
         options: [Array, Object],
         keyType: {
@@ -36,8 +37,10 @@ export default {
     },
     methods: {
         handleChange(tag) {
-            this.selectedTag = tag
-            this.$emit('change', isNaN(parseInt(tag)) || this.keyType === 'string' ? tag : parseInt(tag))
+            if (!this.disabled) {
+                this.selectedTag = tag
+                this.$emit('change', isNaN(parseInt(tag)) || this.keyType === 'string' ? tag : parseInt(tag))
+            }
         }
     },
     watch: {
@@ -55,6 +58,7 @@ export default {
 .ant-tag {
     background-color: rgba(0, 0, 0, 0.05);
 }
+
 .ant-tag-checkable-checked {
     background-color: #1890ff;
 }

+ 57 - 12
frontend/src/components/StdDataEntry/StdDataEntry.vue

@@ -7,7 +7,12 @@
             :validate-status="error[d.dataIndex] ? 'error' :'success'"
             :wrapperCol="d.edit.wrapperCol"
         >
-            <a-input v-if="d.edit.type==='input'" v-model="dataSource[d.dataIndex]" :placeholder="d.edit.placeholder" />
+            <p v-if="d.description" v-html="d.description+'<br/>'"/>
+            <a-input
+                v-if="d.edit.type==='input'"
+                v-model="dataSource[d.dataIndex]"
+                :placeholder="getInputPlaceholder(d, dataSource)"
+            />
             <a-textarea v-else-if="d.edit.type==='textarea'" v-model="dataSource[d.dataIndex]"
                         :rows="d.edit.row?d.edit.row:5"/>
             <std-select-option
@@ -24,13 +29,21 @@
                 :options="d.mask"
             />
 
+            <std-multi-check-tag
+                v-else-if="d.edit.type==='multi-check-tag'"
+                v-model="temp[d.dataIndex]"
+                :data-object="temp"
+                :options="d.mask"
+            />
+
             <std-selector
                 v-else-if="d.edit.type==='selector'" v-model="temp[d.dataIndex]" :api="d.edit.api"
                 :columns="d.edit.columns"
                 :data_key="d.edit.data_key"
                 :disable_search="d.edit.disable_search" :pagination_method="d.edit.pagination_method"
                 :record-value-index="d.edit.recordValueIndex" :value="fn(temp, d.edit.valueIndex)"
-                :get_params="{...d.edit.get_params, ...bindModel(d.edit.bind, temp)}"
+                :get_params="get_params_fn(d)"
+                :description="d.edit.description"
                 selection-type="radio"
             />
 
@@ -45,8 +58,7 @@
                         :crop="d.edit.crop"
                         :auto-upload="d.edit.auto_upload"
                         :crop-options="d.edit.cropOptions" :type="d.edit.upload_type ? d.edit.upload_type : 'img'"
-                        @changeFileUrl="url => {$emit('change_'+d.dataIndex, url)}"
-                        @uploaded="url => {$emit(d.dataIndex+'Uploaded', url)}"
+                        @uploaded="url => {$emit('uploaded', url)}"
             />
 
             <std-date-picker v-else-if="d.edit.type==='date_picker'" v-model="temp[d.dataIndex]"
@@ -72,6 +84,20 @@
                 {{ d.text }}
             </a-checkbox>
 
+            <std-check-group
+                v-else-if="d.edit.type==='check-group'"
+                v-model="temp[d.dataIndex]"
+                :options="d.options"
+                :allow-other="d.edit.allow_other"
+            />
+
+            <std-radio-group
+                v-else-if="d.edit.type==='radio-group'"
+                v-model="temp[d.dataIndex]"
+                :options="d.options"
+                :key-type="d.edit.key_type"
+            />
+
             <std-transfer
                 v-else-if="d.edit.type==='transfer'"
                 v-model="temp[d.dataIndex]"
@@ -79,14 +105,15 @@
                 :data-key="d.edit.dataKey"
             />
 
-            <rich-text-editor v-else-if="d.edit.type==='rich-text'" v-model="temp[d.dataIndex]" />
+            <rich-text-editor v-else-if="d.edit.type==='rich-text'" v-model="temp[d.dataIndex]"/>
 
             <p v-else-if="d.edit.type==='readonly'">
                 {{ d.mask ? d.mask[fn(temp, d.dataIndex)] : fn(temp, d.dataIndex) }}
             </p>
 
-            <p v-else>{{ "edit.type 参数非法 " + d.edit.type }}</p>
+            <p v-else>{{ 'edit.type 参数非法 ' + d.edit.type }}</p>
 
+            <p v-if="!dataSource[d.dataIndex] && d.empty_description" v-html="d.empty_description"/>
         </a-form-item>
         <a-form-item>
             <slot name="supplement"/>
@@ -101,12 +128,18 @@ import StdSelector from './StdSelector'
 import StdUpload from './StdUpload'
 import StdDatePicker from './StdDatePicker'
 import StdTransfer from './StdTransfer'
-import RichTextEditor from "@/components/RichText/RichTextEditor";
-import StdCheckTag from "@/components/StdDataEntry/StdCheckTag";
+import RichTextEditor from '@/components/RichText/RichTextEditor'
+import StdCheckTag from '@/components/StdDataEntry/StdCheckTag'
+import StdMultiCheckTag from '@/components/StdDataEntry/StdMultiCheckTag'
+import StdCheckGroup from '@/components/StdDataEntry/StdCheckGroup'
+import StdRadioGroup from '@/components/StdDataEntry/StdRadioGroup'
 
 export default {
     name: 'StdDataEntry',
     components: {
+        StdRadioGroup,
+        StdCheckGroup,
+        StdMultiCheckTag,
         StdCheckTag,
         RichTextEditor,
         StdTransfer,
@@ -144,14 +177,14 @@ export default {
     },
     watch: {
         dataSource() {
-            this.temp = this.dataSource
+            this.temp = this.dataSource ?? []
         },
         dataList() {
-            this.M_dataList = this.editableColumns(this.dataList)
+            this.M_dataList = this.editableColumns(this.dataList ?? [])
         }
     },
     created() {
-        this.temp = this.dataSource
+        this.temp = this.dataSource ?? []
         if (this.layout === 'horizontal') {
             this.labelCol = {span: 4}
             this.wrapperCol = {span: 18}
@@ -159,11 +192,14 @@ export default {
         this.M_dataList = this.editableColumns(this.dataList)
     },
     methods: {
+        get_params_fn(d) {
+            return {...d.edit.get_params, ...this.bindModel(d.edit.bind, this.temp)}
+        },
         fn: (obj, desc) => {
             const arr = desc.split('.')
             while (arr.length) {
                 const top = obj[arr.shift()]
-                if (!top) {
+                if (top === undefined) {
                     return null
                 }
                 obj = top
@@ -186,6 +222,15 @@ export default {
                 }
             }
             return object
+        },
+        getInputPlaceholder(d, dataSource) {
+            // edit 模式
+            if (dataSource.id) {
+                return d.edit.placeholder?.edit ?? d.edit.placeholder
+            } else {
+                // add 模式
+                return d.edit.placeholder?.add ?? d.edit.placeholder
+            }
         }
     }
 }

+ 75 - 0
frontend/src/components/StdDataEntry/StdMultiCheckTag.vue

@@ -0,0 +1,75 @@
+<template>
+    <div>
+        <template v-for="(v,k) in options">
+            <a-checkable-tag
+                :key="k"
+                :checked="selectedTags.indexOf(k) > -1"
+                @change="checked => handleChange(k, checked)"
+            >
+                {{ v }}
+            </a-checkable-tag>
+        </template>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdMultiCheckTag',
+    data() {
+        return {
+            selectedTags: [],
+        }
+    },
+    props: {
+        disabled: [Boolean],
+        value: [Array],
+        dataObject: [Object],
+        options: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+    },
+    model: {
+        prop: 'value',
+        event: 'change'
+    },
+    methods: {
+        handleChange(tag, checked) {
+            const {selectedTags} = this
+            this.selectedTags = checked
+                ? [...selectedTags, tag]
+                : selectedTags.filter(t => t !== tag)
+            this.$emit('change', this.selectedTags)
+        },
+        loadData() {
+            for (const [k] of Object.entries(this.options)) {
+                if (this.dataObject[k] === 1) {
+                    if (this.selectedTags.indexOf(k) === -1)
+                        this.selectedTags.push(k)
+                }
+            }
+        }
+    },
+    watch: {
+        value() {
+            this.selectedTag = this.value ?? []
+        },
+    },
+    created() {
+        this.selectedTag = this.value ?? []
+        this.loadData()
+    },
+}
+</script>
+
+<style lang="less" scoped>
+.ant-tag {
+    background-color: rgba(0, 0, 0, 0.05);
+}
+
+.ant-tag-checkable-checked {
+    background-color: #1890ff;
+}
+</style>

+ 32 - 8
frontend/src/components/StdDataEntry/StdMultiFilesUpload.vue

@@ -7,8 +7,19 @@
             :file-list="uploadList"
             :remove="remove"
         >
-            <a-button :disabled="disabled"><a-icon type="upload"/>选择文件</a-button>
+            <a-button :disabled="disabled">
+                <a-icon type="upload"/>
+                选择文件
+            </a-button>
         </a-upload>
+        <a-progress
+            v-if="show_progress"
+            :stroke-color="{
+        from: '#108ee9',
+        to: '#87d068',
+      }"
+            :percent="progress"
+        />
         <a-button
             type="primary"
             :disabled="uploadList.length === 0 && !id"
@@ -37,7 +48,7 @@
 
 <script>
 export default {
-    name: "StdMultiFilesUpload",
+    name: 'StdMultiFilesUpload',
     props: {
         api: Function,
         id: {
@@ -67,11 +78,13 @@ export default {
     },
     data() {
         return {
+            show_progress: false,
             uploadList: [],
             uploaded: this.fileList,
             lastFileTime: 0,
-            server: process.env["VUE_APP_API_UPLOAD_ROOT"],
+            server: process.env['VUE_APP_API_UPLOAD_ROOT'],
             uploading: false,
+            progress: 0
         }
     },
     model: {
@@ -82,14 +95,25 @@ export default {
         async upload() {
             if (this.uploadList.length) {
                 this.uploading = true
+                this.show_progress = true
+                this.progress = 0
                 let formData = new FormData()
-                while (this.uploadList.length) {
-                    formData.append('file[]', this.uploadList.shift())
-                }
+                this.uploadList.forEach(v => {
+                    formData.append('file[]', v)
+                })
                 this.visible = false
                 this.uploading = true
                 this.$message.info('正在上传附件, 请不要关闭本页')
-                return this.api(this.id, formData).then(r => {
+                let config = {
+                    onUploadProgress: (progressEvent) => {
+                        // 使用本地 progress 事件
+                        if (progressEvent.lengthComputable) {
+                            this.progress = Math.round((progressEvent.loaded / progressEvent.total) * 100) // 使用某种 UI 进度条组件会用到的百分比
+                        }
+                    }
+                }
+                return this.api(this.id, formData, config).then(r => {
+                    this.uploadList = []
                     this.uploaded = [...this.uploaded, ...r]
                     this.uploading = false
                     this.$emit('uploaded', r)
@@ -112,7 +136,7 @@ export default {
         },
         getFileName(path) {
             // 从15开始找
-            const idx = path.indexOf("/", 15)
+            const idx = path.indexOf('/', 15)
             return path.substring(idx + 1)
         },
         remove(r) {

+ 49 - 0
frontend/src/components/StdDataEntry/StdRadioGroup.vue

@@ -0,0 +1,49 @@
+<template>
+    <a-radio-group name="radioGroup" v-model="data" @change="onChange">
+        <a-radio :value="k" v-for="(v,k) in options" :key="k">
+            {{ v }}
+        </a-radio>
+    </a-radio-group>
+</template>
+
+
+<script>
+export default {
+    name: 'StdRadioGroup',
+    props: {
+        options: [Object,Array],
+        value: {
+            type: [String, Number]
+        },
+        keyType: String
+    },
+    model: {
+        prop: 'value',
+        event: 'changeValue'
+    },
+    data() {
+        return {
+            data: this.value?.toString() ?? '',
+        }
+    },
+    watch: {
+        value() {
+            this.data = this.value.toString()
+        }
+    },
+    methods: {
+        onChange(e) {
+            if (this.keyType === 'int') {
+                this.data = e.target.value
+                this.$emit('changeValue', parseInt(e.target.value))
+            } else {
+                this.$emit('changeValue', e.target.value)
+            }
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 52 - 12
frontend/src/components/StdDataEntry/StdSelector.vue

@@ -1,10 +1,9 @@
 <template>
-    <div class="std-selector" @click="visible=true">
+    <div class="std-selector" @click="show()">
         <a-input v-model="_key" disabled hidden/>
-        <a-input
-            v-model="M_value"
-            disabled
-        />
+        <div class="value">
+            <p>{{ M_value }}</p>
+        </div>
         <a-modal
             :mask="false"
             :visible="visible"
@@ -16,6 +15,7 @@
             :width="600"
             destroyOnClose
         >
+            {{ description }}
             <std-table
                 :api="api"
                 :columns="columns"
@@ -24,8 +24,9 @@
                 :pithy="true"
                 :get_params="get_params"
                 :selectionType="selectionType"
+                :disable_query_params="true"
                 @selected="onSelect"
-                @selectedRecord="r => {record = r}"
+                @selectedRecord="onSelectedRecord"
             />
         </a-modal>
     </div>
@@ -61,7 +62,8 @@ export default {
             default() {
                 return {}
             }
-        }
+        },
+        description: String
     },
     model: {
         prop: '_key',
@@ -86,9 +88,15 @@ export default {
         }
     },
     methods: {
+        show() {
+            this.visible = true
+        },
         onSelect(selected) {
             this.selected = selected
         },
+        onSelectedRecord(r) {
+            this.record = r
+        },
         ok() {
             this.visible = false
             let selected = this.selected
@@ -102,15 +110,47 @@ export default {
 }
 </script>
 
+<style scoped>
+.ant-form-inline .std-selector {
+    height: 40px;
+}
+</style>
+
 <style lang="less" scoped>
 .std-selector {
-    .ant-input {
+    height: 38px;
+    min-width: 180px;
+    position: relative;
+
+    .value {
+        box-sizing: border-box;
+        font-variant: tabular-nums;
+        list-style: none;
+        font-feature-settings: 'tnum';
+        position: absolute;
+        top: 50%;
+        bottom: 50%;
+        left: 50%;
+        -webkit-transform: translateX(-50%) translateY(-50%);
+        display: inline-block;
+        width: 100%;
+        height: 32px;
+        padding: 4px 11px;
+        color: rgba(0, 0, 0, 0.65);
+        font-size: 14px;
+        line-height: 1.5;
+        background-color: #fff;
+        background-image: none;
+        border: 1px solid #d9d9d9;
+        border-radius: 4px;
+        transition: all 0.3s;
         margin: 0 10px 0 0;
         cursor: pointer;
-    }
-    .ant-input-disabled {
-        background: unset;
-        color: unset;
+        @media (prefers-color-scheme: dark) {
+            background-color: #1e1f20;
+            border: 1px solid #666666;
+            color: rgba(255, 255, 255, 0.99);
+        }
     }
 }
 </style>

+ 30 - 8
frontend/src/components/StdDataEntry/StdSingleFileUpload.vue

@@ -3,21 +3,29 @@
         <a-upload
             :before-upload="beforeUpload"
             :multiple="false"
-            :show-upload-list="true"
             :file-list="uploadList"
+            :remove="remove"
         >
-            <a-button :disabled="disabled"><a-icon type="upload"/>上传</a-button>
+            <a-button :disabled="disabled">
+                <a-icon type="upload"/>
+                上传
+            </a-button>
         </a-upload>
+        <a-progress
+            v-if="show_progress"
+            :stroke-color="{from: '#108ee9',to: '#87d068'}"
+            :percent="progress"
+        />
         <p style="margin: 15px 0" v-show="fileUrl">
             <a-icon type="paper-clip" style="margin-right: 5px"/>
-            <a :href="server + '/' + fileUrl" target="_blank" @click="()=>{}">{{ fileUrl }}</a>
+            <a :href="server + '/' + fileUrl" target="_blank" @click="()=>{}">下载附件</a>
         </p>
     </div>
 </template>
 
 <script>
 export default {
-    name: "StdSingleFileUpload",
+    name: 'StdSingleFileUpload',
     props: {
         api: Function,
         id: {
@@ -39,7 +47,9 @@ export default {
     data() {
         return {
             uploadList: [],
-            server: process.env["VUE_APP_API_UPLOAD_ROOT"],
+            server: process.env['VUE_APP_API_UPLOAD_ROOT'],
+            progress: 0,
+            show_progress: false,
         }
     },
     model: {
@@ -47,15 +57,27 @@ export default {
         event: 'changeFileUrl'
     },
     methods: {
+        remove() {
+            this.uploadList.shift()
+        },
         async upload() {
             if (this.uploadList.length) {
+                this.show_progress = true
+                this.progress = 0
                 const formData = new FormData()
                 formData.append('file', this.uploadList.shift())
                 this.visible = false
                 this.uploading = true
                 this.$message.info('正在上传附件, 请不要关闭本页')
-
-                return this.api(this.id, formData).then(r => {
+                let config = {
+                    onUploadProgress: (progressEvent) => {
+                        // 使用本地 progress 事件
+                        if (progressEvent.lengthComputable) {
+                            this.progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
+                        }
+                    }
+                }
+                return this.api(this.id, formData, config).then(r => {
                     this.$emit('uploaded', r.url)
                     this.$emit('changeFileUrl', r.url)
                     this.uploading = false
@@ -65,7 +87,7 @@ export default {
                 })
             }
         },
-       beforeUpload(file) {
+        beforeUpload(file) {
             this.uploadList = [file]
             this.$emit('changeFileUrl', file.name)
             // 有自动上传参数就自动上传,没有就看 id, 没有 id 就不上传

+ 13 - 7
frontend/src/components/StdDataEntry/StdUpload.vue

@@ -50,6 +50,7 @@
             :api="api"
             :auto-upload="autoUpload"
             @changeFileUrl="url => {$emit('changeFileUrl', url)}"
+            @uploaded="url => {$emit('uploaded', url)}"
             :disabled="disabled"
             ref="single-file"
         />
@@ -63,6 +64,7 @@
             :auto-upload="autoUpload"
             :api_delete="api_delete"
             @changeFileUrl="url => {$emit('changeFileUrl', url)}"
+            @uploaded="url => {$emit('uploaded', url)}"
             :disabled="disabled"
             ref="multi-file"
         />
@@ -73,9 +75,9 @@
 <script>
 import Vue from 'vue'
 import VueCropper from 'vue-cropper'
-import StdSingleFileUpload from "@/components/StdDataEntry/StdSingleFileUpload";
-import StdMultiFilesUpload from "@/components/StdDataEntry/StdMultiFilesUpload";
-import { v4 as uuidv4 } from 'uuid';
+import StdSingleFileUpload from '@/components/StdDataEntry/StdSingleFileUpload'
+import StdMultiFilesUpload from '@/components/StdDataEntry/StdMultiFilesUpload'
+import {v4 as uuidv4} from 'uuid'
 
 Vue.use(VueCropper)
 
@@ -134,7 +136,7 @@ export default {
             visible: false,
             fileList: [],
             M_list: this.list,
-            server: process.env["VUE_APP_API_UPLOAD_ROOT"]
+            server: process.env['VUE_APP_API_UPLOAD_ROOT']
         }
     },
     created() {
@@ -151,18 +153,18 @@ export default {
     },
     methods: {
         getFileUrl() {
-            return this.fileUrl.substring(0,5) === 'data:' ? this.fileUrl :
+            return this.fileUrl.substring(0, 5) === 'data:' ? this.fileUrl :
                 this.server + '/' + this.fileUrl
         },
         async upload() {
             if (this.type === 'multi-file') {
-                return await this.$refs["multi-file"].upload()
+                return await this.$refs['multi-file'].upload()
             }
             if (this.orig && this.fileUrl !== this.orig) {
                 return this.handleSingleUpload()
             }
             if (this.$refs['single-file']) {
-                return await this.$refs["single-file"].upload()
+                return await this.$refs['single-file'].upload()
             }
         },
         handleSingleUpload() {
@@ -193,6 +195,10 @@ export default {
                     file.thumbUrl = e.target.result
                     this.$emit('changeFileUrl', e.target.result)
                 }
+                if (this.autoUpload) {
+                    this.handleSingleUpload()
+                    return false
+                }
             } else {
                 this.$emit('changeFileUrl', file.name)
             }

+ 11 - 11
frontend/src/router/index.js

@@ -14,7 +14,7 @@ export const routes = [
         children: [
             {
                 path: 'dashboard',
-                component: () => import('@/views/DashBoard'),
+                component: () => import('@/views/doashboard/DashBoard'),
                 name: '仪表盘',
                 meta: {
                     //hiddenHeaderContent: true,
@@ -24,7 +24,7 @@ export const routes = [
             {
                 path: 'user',
                 name: '用户管理',
-                component: () => import('@/views/User.vue'),
+                component: () => import('@/views/user/User.vue'),
                 meta: {
                     icon: 'user'
                 },
@@ -40,15 +40,15 @@ export const routes = [
                 children: [{
                     path: 'list',
                     name: '网站列表',
-                    component: () => import('@/views/Domain.vue'),
+                    component: () => import('@/views/domain/DomainList.vue'),
                 }, {
                     path: 'add',
                     name: '添加站点',
-                    component: () => import('@/views/domain_edit/DomainEdit.vue'),
+                    component: () => import('@/views/domain/DomainAdd.vue'),
                 }, {
                     path: ':name',
                     name: '编辑站点',
-                    component: () => import('@/views/domain_edit/DomainEdit.vue'),
+                    component: () => import('@/views/domain/DomainEdit.vue'),
                     meta: {
                         hiddenInSidebar: true
                     }
@@ -57,7 +57,7 @@ export const routes = [
             {
                 path: 'config',
                 name: '配置管理',
-                component: () => import('@/views/Config.vue'),
+                component: () => import('@/views/config/Config.vue'),
                 meta: {
                     icon: 'file'
                 },
@@ -65,7 +65,7 @@ export const routes = [
             {
                 path: 'config/:name',
                 name: '配置编辑',
-                component: () => import('@/views/ConfigEdit.vue'),
+                component: () => import('@/views/config/ConfigEdit.vue'),
                 meta: {
                     hiddenInSidebar: true
                 },
@@ -73,7 +73,7 @@ export const routes = [
             {
                 path: 'about',
                 name: '关于',
-                component: () => import('@/views/About.vue'),
+                component: () => import('@/views/other/About.vue'),
                 meta: {
                     icon: 'info-circle'
                 }
@@ -83,19 +83,19 @@ export const routes = [
     {
         path: '/install',
         name: '安装',
-        component: () => import('@/views/Install'),
+        component: () => import('@/views/other/Install'),
         meta: {noAuth: true}
     },
     {
         path: '/login',
         name: '登录',
-        component: () => import('@/views/Login'),
+        component: () => import('@/views/other/Login'),
         meta: {noAuth: true}
     },
     {
         path: '/404',
         name: '404 Not Found',
-        component: () => import('@/views/Error'),
+        component: () => import('@/views/other/Error'),
         meta: {noAuth: true, status_code: 404, error: 'Not Found'}
     },
     {

+ 0 - 119
frontend/src/views/Login.vue

@@ -1,119 +0,0 @@
-<template>
-    <div class="login-form">
-        <div class="project-title">
-            <h1>Nginx UI</h1>
-        </div>
-        <a-form
-            id="components-form-demo-normal-login"
-            :form="form"
-            class="login-form"
-            @submit="handleSubmit"
-        >
-            <a-form-item>
-                <a-input
-                    v-decorator="[
-          'name',
-          { rules: [{ required: true, message: 'Please input your username!' }] },
-        ]"
-                    placeholder="Username"
-                >
-                    <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
-                </a-input>
-            </a-form-item>
-            <a-form-item>
-                <a-input
-                    v-decorator="[
-          'password',
-          { rules: [{ required: true, message: 'Please input your Password!' }] },
-        ]"
-                    type="password"
-                    placeholder="Password"
-                >
-                    <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
-                </a-input>
-            </a-form-item>
-            <a-form-item>
-                <a-button type="primary" :block="true" html-type="submit" :loading="loading">
-                    登录
-                </a-button>
-            </a-form-item>
-        </a-form>
-        <footer>
-            Copyright © 2020 - {{ thisYear }} 0xJacky
-        </footer>
-    </div>
-
-</template>
-
-<script>
-export default {
-    name: 'Login',
-    data() {
-        return {
-            form: {},
-            thisYear: new Date().getFullYear(),
-            loading: false
-        }
-    },
-    created() {
-        this.form = this.$form.createForm(this)
-    },
-    mounted() {
-        this.$api.install.get_lock().then(r=>{
-            if (!r.lock) {
-                this.$router.push('/install')
-            }
-        })
-        if (this.$store.state.user.token) {
-            this.$router.push('/')
-        }
-    },
-    methods: {
-        login(values) {
-            return this.$api.auth.login(values.name, values.password).then(async () => {
-                await this.$message.success('登录成功', 1)
-                const next = this.$route.query.next ? this.$route.query.next : '/'
-                await this.$router.push(next)
-            }).catch(r => {
-                console.log(r)
-                this.$message.error(r.message ?? '服务器错误')
-            })
-        },
-        handleSubmit: async function (e) {
-            e.preventDefault()
-            this.loading = true
-            await this.form.validateFields(async (err, values) => {
-                if (!err) {
-                    await this.login(values)
-                }
-                this.loading = false
-            })
-        },
-    },
-};
-</script>
-<style lang="less">
-.project-title {
-    margin: 50px;
-
-    h1 {
-        font-size: 50px;
-        font-weight: 100;
-        text-align: center;
-    }
-}
-
-.login-form {
-    max-width: 500px;
-    margin: 0 auto;
-}
-
-.login-form-button {
-
-}
-
-footer {
-    padding: 30px;
-    text-align: center;
-}
-</style>

+ 0 - 0
frontend/src/views/Config.vue → frontend/src/views/config/Config.vue


+ 0 - 0
frontend/src/views/ConfigEdit.vue → frontend/src/views/config/ConfigEdit.vue


+ 0 - 0
frontend/src/views/DashBoard.vue → frontend/src/views/doashboard/DashBoard.vue


+ 1 - 1
frontend/src/views/domain_edit/CertInfo.vue → frontend/src/views/domain/CertInfo.vue

@@ -50,6 +50,6 @@ export default {
 }
 </script>
 
-<style scoped>
+<style lang="less" scoped>
 
 </style>

+ 65 - 0
frontend/src/views/domain/DomainAdd.vue

@@ -0,0 +1,65 @@
+<template>
+    <a-card title="添加站点">
+        <p>在这里添加站点,添加完成后进入域名配置编辑页面即可配置 SSL</p>
+        <std-data-entry :data-list="columns" :data-source="config"/>
+        <footer-tool-bar>
+            <a-button
+                type="primary"
+                @click="save"
+            >
+                完成
+            </a-button>
+        </footer-tool-bar>
+    </a-card>
+</template>
+
+<script>
+import FooterToolBar from "@/components/FooterToolbar/FooterToolBar"
+import StdDataEntry from "@/components/StdDataEntry/StdDataEntry"
+import {columns} from "@/views/domain/columns"
+import {unparse} from "@/views/domain/methods"
+
+export default {
+    name: "DomainAdd",
+    components: {StdDataEntry, FooterToolBar},
+    data() {
+        return {
+            config: {},
+            columns: columns.slice(0, -1) // 隐藏SSL支持开关
+        }
+    },
+    beforeCreate() {
+
+    },
+    methods: {
+        save() {
+            this.$api.domain.get_template('http-conf').then(r => {
+                let text = unparse(r.template, this.config)
+                this.$api.domain.save(this.config.name, {content: text, enabled: true}).then(() => {
+                    this.$message.success("保存成功")
+
+                    this.$api.domain.enable(this.config.name).then(() => {
+                        this.$message.success("启用成功")
+
+                        this.$router.push('/domain/' + this.config.name)
+
+                    }).catch(r => {
+                        console.log(r)
+                        this.$message.error(r.message ?? '启用失败', 10)
+                    })
+
+                }).catch(r => {
+                    console.log(r)
+                    this.$message.error(r.message ?? '保存错误', 10)
+                })
+            })
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-steps {
+    padding: 10px 0 20px 0;
+}
+</style>

+ 66 - 47
frontend/src/views/domain_edit/DomainEdit.vue → frontend/src/views/domain/DomainEdit.vue

@@ -3,14 +3,18 @@
         <a-collapse :bordered="false" default-active-key="1">
             <a-collapse-panel key="1" :header="name ? '编辑站点:' + name : '添加站点'">
                 <p>您的配置文件中应当有对应的字段时,下列表单中的设置才能生效,配置文件名称创建后不可修改。</p>
-                <std-data-entry :data-list="columns" v-model="config" />
-                <cert-info :domain="name" ref="cert-info" v-if="name"/>
-                <br/>
-                <a-space>
+                <std-data-entry :data-list="columns" v-model="config"/>
+                <template v-if="config.support_ssl">
+                    <cert-info :domain="name" ref="cert-info" v-if="name"/>
+                    <br/>
                     <a-button @click="issue_cert" type="primary" ghost>
                         自动申请 Let's Encrypt 证书
                     </a-button>
-                </a-space>
+                    <p><br/>点击自动申请证书将会从 Let's Encrypt 获得签发证书
+                        在获取签发证书前,请确保配置文件中已为
+                        <code>/.well-known</code> 目录反向代理到后端的
+                        <code>HTTPChallengePort (default:9180)</code></p>
+                </template>
             </a-collapse-panel>
         </a-collapse>
 
@@ -20,7 +24,7 @@
 
         <footer-tool-bar>
             <a-space>
-                <a-button @click="$router.go(-1)">返回</a-button>
+                <a-button @click="$router.push('/domain/list')">返回</a-button>
                 <a-button type="primary" @click="save">保存</a-button>
             </a-space>
         </footer-tool-bar>
@@ -32,16 +36,16 @@
 import StdDataEntry from "@/components/StdDataEntry/StdDataEntry"
 import FooterToolBar from "@/components/FooterToolbar/FooterToolBar"
 import VueItextarea from "@/components/VueItextarea/VueItextarea"
-import columns from "@/views/domain_edit/columns"
-import CertInfo from "@/views/domain_edit/CertInfo";
+import {columns, columnsSSL} from "@/views/domain/columns"
+import {unparse} from "@/views/domain/methods"
+import CertInfo from "@/views/domain/CertInfo"
 
 export default {
     name: "DomainEdit",
     components: {CertInfo, FooterToolBar, StdDataEntry, VueItextarea},
     data() {
         return {
-            name: this.$route.params.name,
-            columns,
+            name: this.$route.params.name.toString(),
             config: {
                 http_listen_port: 80,
                 https_listen_port: null,
@@ -50,7 +54,8 @@ export default {
                 root: "",
                 ssl_certificate: "",
                 ssl_certificate_key: "",
-                support_ssl: false
+                support_ssl: false,
+                auto_cert: false
             },
             configText: "",
             ws: null,
@@ -68,7 +73,14 @@ export default {
             deep: true
         },
         'config.support_ssl'() {
-            if (this.ok) this.change_support_ssl()
+            if (this.ok) {
+                this.change_support_ssl()
+            }
+        },
+        'config.auto_cert'() {
+            if (this.ok) {
+                this.change_auto_cert()
+            }
         }
     },
     created() {
@@ -84,7 +96,8 @@ export default {
             if (this.name) {
                 this.$api.domain.get(this.name).then(r => {
                     this.configText = r.config
-                    this.parse(r).then(()=>{
+                    this.config.auto_cert = r.auto_cert
+                    this.parse(r).then(() => {
                         this.ok = true
                     })
                 }).catch(r => {
@@ -100,7 +113,8 @@ export default {
                     root: "",
                     ssl_certificate: "",
                     ssl_certificate_key: "",
-                    support_ssl: false
+                    support_ssl: false,
+                    auto_cert: false,
                 }
                 this.get_template()
             }
@@ -133,32 +147,7 @@ export default {
             }
         },
         async unparse() {
-            let text = this.configText
-            // http_listen_port: /listen (.*);/i,
-            // https_listen_port: /listen (.*) ssl/i,
-            const reg = {
-                server_name: /server_name[\s](.*);/ig,
-                index: /index[\s](.*);/i,
-                root: /root[\s](.*);/i,
-                ssl_certificate: /ssl_certificate[\s](.*);/i,
-                ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
-            }
-            text = text.replace(/listen[\s](.*);/i, "listen\t"
-                + this.config['http_listen_port'] + ';')
-            text = text.replace(/listen[\s](.*) ssl/i, "listen\t"
-                + this.config['https_listen_port'] + ' ssl')
-
-            text = text.replace(/listen(.*):(.*);/i, "listen\t[::]:"
-                + this.config['http_listen_port'] + ';')
-            text = text.replace(/listen(.*):(.*) ssl/i, "listen\t[::]:"
-                + this.config['https_listen_port'] + ' ssl')
-
-            for (let k in reg) {
-                text = text.replace(new RegExp(reg[k]), k + "\t" +
-                    (this.config[k] !== undefined ? this.config[k] : " ") + ";")
-            }
-
-            this.configText = text
+            this.configText = unparse(this.configText, this.config)
         },
         async get_template() {
             if (this.config.support_ssl) {
@@ -185,11 +174,11 @@ export default {
             })
         },
         save() {
-            this.$api.domain.save(this.name ? this.name : this.config.name, {content: this.configText}).then(r => {
+            this.$api.domain.save(this.name, {content: this.configText}).then(r => {
                 this.parse(r)
                 this.$message.success("保存成功")
                 if (this.name) {
-                    this.$refs["cert-info"].get()
+                    if (this.$refs["cert-info"]) this.$refs["cert-info"].get()
                 }
             }).catch(r => {
                 console.log(r)
@@ -223,10 +212,35 @@ export default {
                 if (r.status === "success" && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
                     this.config.ssl_certificate = r.ssl_certificate
                     this.config.ssl_certificate_key = r.ssl_certificate_key
-                    this.$refs["cert-info"].get()
+                    if (this.$refs["cert-info"]) this.$refs["cert-info"].get()
+                }
+            }
+        },
+        change_auto_cert() {
+            if (this.config.auto_cert) {
+                this.$api.domain.add_auto_cert(this.name).then(() => {
+                    this.$message.success(this.name + ' 加入自动续签列表成功')
+                }).catch(e => {
+                    this.$message.error(e.message ?? this.name + ' 加入自动续签列表失败')
+                })
+            } else {
+                this.$api.domain.remove_auto_cert(this.name).then(() => {
+                    this.$message.success('从自动续签列表中删除 ' + this.name + ' 成功')
+                }).catch(e => {
+                    this.$message.error(e.message ?? '从自动续签列表中删除 ' + this.name + ' 失败')
+                })
+            }
+        }
+    },
+    computed: {
+        columns: {
+            get() {
+                if (this.config.support_ssl) {
+                    return [...columns, ...columnsSSL]
+                } else {
+                    return [...columns]
                 }
             }
-
         }
     }
 }
@@ -234,16 +248,21 @@ export default {
 
 <style lang="less">
 .ant-collapse {
-    margin: 10px;
+    margin-bottom: 20px;
+
+    .ant-collapse-item {
+        border-bottom: unset;
+    }
 }
+
 .ant-collapse-content-box {
-    padding: 24px!important;
+    padding: 24px !important;
 }
 </style>
 
 <style lang="less" scoped>
 .ant-card {
-    margin: 10px;
+    // margin: 10px;
     @media (max-width: 512px) {
         margin: 10px 0;
     }

+ 1 - 1
frontend/src/views/Domain.vue → frontend/src/views/domain/DomainList.vue

@@ -24,7 +24,7 @@
 import StdTable from "@/components/StdDataDisplay/StdTable"
 
 const columns = [{
-    title: "名称",
+    title: "配置名称",
     dataIndex: "name",
     scopedSlots: {customRender: "名称"},
     sorter: true,

+ 13 - 1
frontend/src/views/domain_edit/columns.js → frontend/src/views/domain/columns.js

@@ -36,6 +36,18 @@ const columns = [{
         type: "switch",
         event: "change_support_ssl"
     }
+}]
+
+const columnsSSL = [{
+    title: "自动续签",
+    dataIndex: "auto_cert",
+    edit: {
+        type: "switch",
+        event: "change_auto_cert"
+    },
+    description: '启用自动续签后,系统将会每小时检测一次该域名证书的信息,' +
+        '如果距离上次签发已超过1个月,则将执行自动续签。' +
+        '<br/>启用前先点击下方「自动申请 Let\'s Encrypt 证书」即可获得证书路径。'
 }, {
     title: "https 监听端口",
     dataIndex: "https_listen_port",
@@ -57,4 +69,4 @@ const columns = [{
     }
 }]
 
-export default columns
+export {columns, columnsSSL}

+ 29 - 0
frontend/src/views/domain/methods.js

@@ -0,0 +1,29 @@
+const unparse = (text, config) => {
+    // http_listen_port: /listen (.*);/i,
+    // https_listen_port: /listen (.*) ssl/i,
+    const reg = {
+        server_name: /server_name[\s](.*);/ig,
+        index: /index[\s](.*);/i,
+        root: /root[\s](.*);/i,
+        ssl_certificate: /ssl_certificate[\s](.*);/i,
+        ssl_certificate_key: /ssl_certificate_key[\s](.*);/i
+    }
+    text = text.replace(/listen[\s](.*);/i, "listen\t"
+        + config['http_listen_port'] + ';')
+    text = text.replace(/listen[\s](.*) ssl/i, "listen\t"
+        + config['https_listen_port'] + ' ssl')
+
+    text = text.replace(/listen(.*):(.*);/i, "listen\t[::]:"
+        + config['http_listen_port'] + ';')
+    text = text.replace(/listen(.*):(.*) ssl/i, "listen\t[::]:"
+        + config['https_listen_port'] + ' ssl')
+
+    for (let k in reg) {
+        text = text.replace(new RegExp(reg[k]), k + "\t" +
+            (config[k] !== undefined ? config[k] : " ") + ";")
+    }
+
+    return text
+}
+
+export {unparse}

+ 11 - 1
frontend/src/views/About.vue → frontend/src/views/other/About.vue

@@ -1,5 +1,8 @@
 <template>
-    <a-card>
+    <a-card style="text-align: center">
+        <div class="logo">
+            <img :src="logo"  alt="logo" />
+        </div>
         <h2>Nginx UI</h2>
         <p>Yet another WebUI for Nginx</p>
         <p>Version: {{ version }} ({{ build_id }})</p>
@@ -22,6 +25,7 @@ export default {
     data() {
         const date = new Date()
         return {
+            logo: require('@/assets/img/logo.png'),
             this_year: date.getFullYear(),
             version: process.env.VUE_APP_VERSION,
             build_id: process.env.VUE_APP_TOTAL_BUILD ?? '开发模式',
@@ -39,6 +43,12 @@ export default {
 </script>
 
 <style lang="less" scoped>
+
+.logo {
+    img {
+        max-width: 120px
+    }
+}
 .egg {
     padding: 10px 0;
 }

+ 0 - 0
frontend/src/views/Error.vue → frontend/src/views/other/Error.vue


+ 0 - 0
frontend/src/views/Home.vue → frontend/src/views/other/Home.vue


+ 0 - 0
frontend/src/views/Install.vue → frontend/src/views/other/Install.vue


+ 125 - 0
frontend/src/views/other/Login.vue

@@ -0,0 +1,125 @@
+<template>
+    <div class="container">
+        <div class="login-form">
+            <div class="project-title">
+                <h1>Nginx UI</h1>
+            </div>
+            <a-form
+                id="components-form-demo-normal-login"
+                :form="form"
+                @submit="handleSubmit"
+            >
+                <a-form-item>
+                    <a-input
+                        v-decorator="[
+          'name',
+          { rules: [{ required: true, message: '请输入用户名' }] },
+        ]"
+                        placeholder="Username"
+                    >
+                        <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
+                    </a-input>
+                </a-form-item>
+                <a-form-item>
+                    <a-input
+                        v-decorator="[
+          'password',
+          { rules: [{ required: true, message: '请输入密码' }] },
+        ]"
+                        type="password"
+                        placeholder="Password"
+                    >
+                        <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
+                    </a-input>
+                </a-form-item>
+                <a-form-item>
+                    <a-button type="primary" :block="true" html-type="submit" :loading="loading">
+                        登录
+                    </a-button>
+                </a-form-item>
+            </a-form>
+            <div class="footer">
+                Copyright © 2020 - {{ thisYear }} 0xJacky
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'Login',
+    data() {
+        return {
+            form: {},
+            thisYear: new Date().getFullYear(),
+            loading: false
+        }
+    },
+    created() {
+        this.form = this.$form.createForm(this)
+    },
+    mounted() {
+        this.$api.install.get_lock().then(r => {
+            if (!r.lock) {
+                this.$router.push('/install')
+            }
+        })
+        if (this.$store.state.user.token) {
+            this.$router.push('/')
+        }
+    },
+    methods: {
+        login(values) {
+            return this.$api.auth.login(values.name, values.password).then(async () => {
+                await this.$message.success('登录成功', 1)
+                const next = this.$route.query.next ? this.$route.query.next : '/'
+                await this.$router.push(next)
+            }).catch(r => {
+                console.log(r)
+                this.$message.error(r.message ?? '服务器错误')
+            })
+        },
+        handleSubmit: async function (e) {
+            e.preventDefault()
+            this.loading = true
+            await this.form.validateFields(async (err, values) => {
+                if (!err) {
+                    await this.login(values)
+                }
+                this.loading = false
+            })
+        },
+    },
+};
+</script>
+<style lang="less">
+.container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    .login-form {
+        max-width: 400px;
+        width: 80%;
+        .project-title {
+            margin: 50px;
+
+            h1 {
+                font-size: 50px;
+                font-weight: 100;
+                text-align: center;
+            }
+        }
+
+        .login-form-button {
+
+        }
+
+        .footer {
+            padding: 30px;
+            text-align: center;
+        }
+    }
+}
+
+</style>

+ 0 - 0
frontend/src/views/User.vue → frontend/src/views/user/User.vue


+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.0.0","build_id":12,"total_build":16}
+{"version":"1.1.0","build_id":2,"total_build":19}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно