mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 10:25:52 +02:00
bug fix
This commit is contained in:
parent
ad421905a8
commit
dd6e19657a
162 changed files with 15071 additions and 932 deletions
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
FROM debian:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./sources.list /etc/apt/sources.list
|
||||
RUN echo "installing nginx"
|
||||
RUN apt-get update -y && apt install nginx -y
|
||||
|
||||
COPY ./start.sh /app/start.sh
|
||||
RUN chmod a+x start.sh
|
||||
COPY ./server /app/server
|
||||
COPY ./html /app/html
|
||||
COPY ./nginx.conf /etc/nginx/sites-available/default
|
||||
EXPOSE 9180
|
||||
|
||||
CMD ["./start.sh"]
|
12
build.sh
Normal file
12
build.sh
Normal file
|
@ -0,0 +1,12 @@
|
|||
echo "buil frontend"
|
||||
cd frontend || exit 1
|
||||
yarn build
|
||||
cd .. || exit 1
|
||||
|
||||
echo "build server"
|
||||
cd server || exit 1
|
||||
GOOS=linux GOARCH=amd64 go build -o nginx-ui@linux-amd64 main.go
|
||||
cd .. || exit 1
|
||||
|
||||
echo "build docker"
|
||||
docker build -t nginx-ui .
|
2
frontend/.env.development
Normal file
2
frontend/.env.development
Normal file
|
@ -0,0 +1,2 @@
|
|||
VUE_APP_API_ROOT = /
|
||||
VUE_APP_API_WSS_ROOT = /ws
|
2
frontend/.env.production
Normal file
2
frontend/.env.production
Normal file
|
@ -0,0 +1,2 @@
|
|||
VUE_APP_API_ROOT = /api
|
||||
VUE_APP_API_WSS_ROOT = /api
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
11356
frontend/dist/img/remixicon.symbol.f09b1c74.svg
vendored
Normal file
11356
frontend/dist/img/remixicon.symbol.f09b1c74.svg
vendored
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 877 KiB |
1
frontend/dist/index.html
vendored
Normal file
1
frontend/dist/index.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta content="IE=edge" http-equiv="X-UA-Compatible"><meta content="width=device-width,initial-scale=1,user-scalable=0" name="viewport"><link href="/favicon.ico" rel="icon"><title>Nginx UI</title><link href="/js/chunk-15551262.ab2ff742.js" rel="prefetch"><link href="/js/chunk-17d07320.f20a6ac5.js" rel="prefetch"><link href="/js/chunk-1e5147a5.69288dbf.js" rel="prefetch"><link href="/js/chunk-228c37e8.3f33dfe8.js" rel="prefetch"><link href="/js/chunk-23e3da44.08117b5e.js" rel="prefetch"><link href="/js/chunk-2b94df79.6a91b4fd.js" rel="prefetch"><link href="/js/chunk-2d0cf277.99e2e71b.js" rel="prefetch"><link href="/js/chunk-532b3473.eaa829bf.js" rel="prefetch"><link href="/js/chunk-56341220.4ea9419d.js" rel="prefetch"><link href="/js/chunk-742ad954.a1467ad6.js" rel="prefetch"><link href="/js/chunk-96068e84.dcec99db.js" rel="prefetch"><link href="/js/chunk-d4ac245c.36cefb3a.js" rel="prefetch"><link href="/js/chunk-e0ad5fdc.02ec34ba.js" rel="prefetch"><link href="/js/chunk-e71b472c.345ce2d9.js" rel="prefetch"><link href="/js/chunk-vendors.ee48c25c.js" rel="modulepreload" as="script"><link href="/js/index.c8496a25.js" rel="modulepreload" as="script"></head><body><noscript><strong>We're sorry but Nginx UI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script type="module" src="/js/chunk-vendors.ee48c25c.js"></script><script type="module" src="/js/index.c8496a25.js"></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src="/js/chunk-vendors-legacy.ee48c25c.js" nomodule></script><script src="/js/index-legacy.a529f66e.js" nomodule></script></body></html>
|
1
frontend/dist/js/chunk-15551262-legacy.ab2ff742.js
vendored
Normal file
1
frontend/dist/js/chunk-15551262-legacy.ab2ff742.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-15551262.ab2ff742.js
vendored
Normal file
1
frontend/dist/js/chunk-15551262.ab2ff742.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-17d07320-legacy.f20a6ac5.js
vendored
Normal file
1
frontend/dist/js/chunk-17d07320-legacy.f20a6ac5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-17d07320.f20a6ac5.js
vendored
Normal file
1
frontend/dist/js/chunk-17d07320.f20a6ac5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-1e5147a5-legacy.69288dbf.js
vendored
Normal file
1
frontend/dist/js/chunk-1e5147a5-legacy.69288dbf.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-1e5147a5"],{"22e7":function(e,t,r){var n=r("24fb");t=n(!1),t.push([e.i,".project-title{margin:50px}.project-title h1{font-size:50px;font-weight:100;text-align:center}.login-form{max-width:500px;margin:0 auto}footer{padding:30px;text-align:center}",""]),e.exports=t},4871:function(e,t,r){var n=r("22e7");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var a=r("499e").default;a("749337a0",n,!0,{sourceMap:!1,shadowMode:!1})},"4fde":function(e,t,r){"use strict";r("4871")},a55b:function(e,t,r){"use strict";r.r(t);var n=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{staticClass:"login-form",attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["name",{rules:[{required:!0,message:"Please input your username!"}]}],expression:"[\n 'name',\n { rules: [{ required: true, message: 'Please input your username!' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"Please input your Password!"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: 'Please input your Password!' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 登录 ")])],1)],1),r("footer",[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],s=r("1da1"),o=(r("96cf"),r("b0c0"),{name:"Login",data:function(){return{form:{},thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.$store.state.user.token?this.$router.push("/"):this.form=this.$form.createForm(this)},methods:{login:function(e){var t=this;this.$api.auth.login(e.name,e.password).then(Object(s["a"])(regeneratorRuntime.mark((function e(){var r;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$message.success("登录成功",1);case 2:return r=t.$route.query.next?t.$route.query.next:"/",e.next=5,t.$router.push(r);case 5:case"end":return e.stop()}}),e)})))).catch((function(e){var r;console.log(e),t.$message.error(null!==(r=e.message)&&void 0!==r?r:"服务器错误")}))},handleSubmit:function(){var e=Object(s["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(s["a"])(regeneratorRuntime.mark((function e(t,n){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t){e.next=3;break}return e.next=3,r.login(n);case 3:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:this.loading=!1;case 5:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),i=o,u=(r("4fde"),r("2877")),c=Object(u["a"])(i,n,a,!1,null,null,null);t["default"]=c.exports}}]);
|
1
frontend/dist/js/chunk-1e5147a5.69288dbf.js
vendored
Normal file
1
frontend/dist/js/chunk-1e5147a5.69288dbf.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-1e5147a5"],{"22e7":function(e,t,r){var n=r("24fb");t=n(!1),t.push([e.i,".project-title{margin:50px}.project-title h1{font-size:50px;font-weight:100;text-align:center}.login-form{max-width:500px;margin:0 auto}footer{padding:30px;text-align:center}",""]),e.exports=t},4871:function(e,t,r){var n=r("22e7");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var a=r("499e").default;a("749337a0",n,!0,{sourceMap:!1,shadowMode:!1})},"4fde":function(e,t,r){"use strict";r("4871")},a55b:function(e,t,r){"use strict";r.r(t);var n=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{staticClass:"login-form",attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["name",{rules:[{required:!0,message:"Please input your username!"}]}],expression:"[\n 'name',\n { rules: [{ required: true, message: 'Please input your username!' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"Please input your Password!"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: 'Please input your Password!' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 登录 ")])],1)],1),r("footer",[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],s=r("1da1"),o=(r("96cf"),r("b0c0"),{name:"Login",data:function(){return{form:{},thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.$store.state.user.token?this.$router.push("/"):this.form=this.$form.createForm(this)},methods:{login:function(e){var t=this;this.$api.auth.login(e.name,e.password).then(Object(s["a"])(regeneratorRuntime.mark((function e(){var r;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$message.success("登录成功",1);case 2:return r=t.$route.query.next?t.$route.query.next:"/",e.next=5,t.$router.push(r);case 5:case"end":return e.stop()}}),e)})))).catch((function(e){var r;console.log(e),t.$message.error(null!==(r=e.message)&&void 0!==r?r:"服务器错误")}))},handleSubmit:function(){var e=Object(s["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(s["a"])(regeneratorRuntime.mark((function e(t,n){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t){e.next=3;break}return e.next=3,r.login(n);case 3:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:this.loading=!1;case 5:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),i=o,u=(r("4fde"),r("2877")),c=Object(u["a"])(i,n,a,!1,null,null,null);t["default"]=c.exports}}]);
|
1
frontend/dist/js/chunk-228c37e8-legacy.3f33dfe8.js
vendored
Normal file
1
frontend/dist/js/chunk-228c37e8-legacy.3f33dfe8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-228c37e8.3f33dfe8.js
vendored
Normal file
1
frontend/dist/js/chunk-228c37e8.3f33dfe8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
frontend/dist/js/chunk-2b94df79-legacy.6a91b4fd.js
vendored
Normal file
3
frontend/dist/js/chunk-2b94df79-legacy.6a91b4fd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
frontend/dist/js/chunk-2b94df79.6a91b4fd.js
vendored
Normal file
3
frontend/dist/js/chunk-2b94df79.6a91b4fd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-532b3473-legacy.eaa829bf.js
vendored
Normal file
1
frontend/dist/js/chunk-532b3473-legacy.eaa829bf.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-532b3473.eaa829bf.js
vendored
Normal file
1
frontend/dist/js/chunk-532b3473.eaa829bf.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-56341220-legacy.4ea9419d.js
vendored
Normal file
1
frontend/dist/js/chunk-56341220-legacy.4ea9419d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-56341220.4ea9419d.js
vendored
Normal file
1
frontend/dist/js/chunk-56341220.4ea9419d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-96068e84-legacy.dcec99db.js
vendored
Normal file
1
frontend/dist/js/chunk-96068e84-legacy.dcec99db.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(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="6")&&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}}]);
|
1
frontend/dist/js/chunk-96068e84.dcec99db.js
vendored
Normal file
1
frontend/dist/js/chunk-96068e84.dcec99db.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(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="6")&&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}}]);
|
1
frontend/dist/js/chunk-d4ac245c-legacy.36cefb3a.js
vendored
Normal file
1
frontend/dist/js/chunk-d4ac245c-legacy.36cefb3a.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-d4ac245c"],{4159:function(t,e,n){var a=n("24fb");e=a(!1),e.push([t.i,".ant-card[data-v-6d72b90c]{margin:10px}@media (max-width:512px){.ant-card[data-v-6d72b90c]{margin:10px 0}}",""]),t.exports=e},9963:function(t,e,n){var a=n("4159");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=n("499e").default;o("7aad0ace",a,!0,{sourceMap:!1,shadowMode:!1})},afa2:function(t,e,n){"use strict";n("9963")},e33d:function(t,e,n){"use strict";n.r(e);var a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("a-card",{attrs:{title:"配置文件编辑"}},[n("vue-itextarea",{model:{value:t.configText,callback:function(e){t.configText=e},expression:"configText"}}),n("footer-tool-bar",[n("a-space",[n("a-button",{on:{click:function(e){return t.$router.go(-1)}}},[t._v("返回")]),n("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],c=(n("b0c0"),n("9c70")),i=n("a002"),s={name:"DomainEdit",components:{FooterToolBar:c["a"],VueItextarea:i["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.config={},this.configText=""},config:{handler:function(){this.unparse()},deep:!0}},created:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(e){t.configText=e.config})).catch((function(e){console.log(e),t.$message.error("服务器错误")})):this.configText=""},methods:{save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(e){t.configText=e.config,t.$message.success("保存成功")})).catch((function(e){console.log(e),t.$message.error("保存错误")}))}}},r=s,f=(n("afa2"),n("2877")),u=Object(f["a"])(r,a,o,!1,null,"6d72b90c",null);e["default"]=u.exports}}]);
|
1
frontend/dist/js/chunk-d4ac245c.36cefb3a.js
vendored
Normal file
1
frontend/dist/js/chunk-d4ac245c.36cefb3a.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-d4ac245c"],{4159:function(t,e,n){var a=n("24fb");e=a(!1),e.push([t.i,".ant-card[data-v-6d72b90c]{margin:10px}@media (max-width:512px){.ant-card[data-v-6d72b90c]{margin:10px 0}}",""]),t.exports=e},9963:function(t,e,n){var a=n("4159");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=n("499e").default;o("7aad0ace",a,!0,{sourceMap:!1,shadowMode:!1})},afa2:function(t,e,n){"use strict";n("9963")},e33d:function(t,e,n){"use strict";n.r(e);var a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("a-card",{attrs:{title:"配置文件编辑"}},[n("vue-itextarea",{model:{value:t.configText,callback:function(e){t.configText=e},expression:"configText"}}),n("footer-tool-bar",[n("a-space",[n("a-button",{on:{click:function(e){return t.$router.go(-1)}}},[t._v("返回")]),n("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],c=(n("b0c0"),n("9c70")),i=n("a002"),s={name:"DomainEdit",components:{FooterToolBar:c["a"],VueItextarea:i["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.config={},this.configText=""},config:{handler:function(){this.unparse()},deep:!0}},created:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(e){t.configText=e.config})).catch((function(e){console.log(e),t.$message.error("服务器错误")})):this.configText=""},methods:{save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(e){t.configText=e.config,t.$message.success("保存成功")})).catch((function(e){console.log(e),t.$message.error("保存错误")}))}}},r=s,f=(n("afa2"),n("2877")),u=Object(f["a"])(r,a,o,!1,null,"6d72b90c",null);e["default"]=u.exports}}]);
|
1
frontend/dist/js/chunk-e0ad5fdc-legacy.02ec34ba.js
vendored
Normal file
1
frontend/dist/js/chunk-e0ad5fdc-legacy.02ec34ba.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-e0ad5fdc.02ec34ba.js
vendored
Normal file
1
frontend/dist/js/chunk-e0ad5fdc.02ec34ba.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-e71b472c-legacy.345ce2d9.js
vendored
Normal file
1
frontend/dist/js/chunk-e71b472c-legacy.345ce2d9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-e71b472c.345ce2d9.js
vendored
Normal file
1
frontend/dist/js/chunk-e71b472c.345ce2d9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/dist/js/index-legacy.a529f66e.js
vendored
Normal file
1
frontend/dist/js/index-legacy.a529f66e.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/index.c8496a25.js
vendored
Normal file
1
frontend/dist/js/index.c8496a25.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/version.json
vendored
Normal file
1
frontend/dist/version.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":"1.0.0","build_id":2,"total_build":6}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "nginx-ui-frontend",
|
||||
"version": "0.1.1",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
@ -15,12 +15,23 @@
|
|||
"reconnecting-websocket": "^4.4.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-itextarea": "^1.0.9",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persist": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/extension-character-count": "^2.0.0-beta.13",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.0-beta.33",
|
||||
"@tiptap/extension-document": "^2.0.0-beta.13",
|
||||
"@tiptap/extension-highlight": "^2.0.0-beta.15",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.16",
|
||||
"@tiptap/extension-task-item": "^2.0.0-beta.17",
|
||||
"@tiptap/extension-task-list": "^2.0.0-beta.17",
|
||||
"@tiptap/extension-text": "^2.0.0-beta.13",
|
||||
"@tiptap/starter-kit": "^2.0.0-beta.90",
|
||||
"@tiptap/vue-2": "^2.0.0-beta.42",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
|
@ -32,9 +43,14 @@
|
|||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.11.1",
|
||||
"less-loader": "^5.0.0",
|
||||
"lowlight": "^1.20.0",
|
||||
"moment": "^2.24.0",
|
||||
"node-sass": "^6.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"vue-cli-plugin-generate-build-id": "0.1.0",
|
||||
"remixicon": "^2.5.0",
|
||||
"sass-loader": "^10",
|
||||
"vue-cli-plugin-generate-build-id": "^0.2.0",
|
||||
"vue-cropper": "^0.5.6",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -11,8 +11,8 @@ const auth = {
|
|||
})
|
||||
},
|
||||
logout() {
|
||||
return http.delete('/logout').then(() => {
|
||||
store.dispatch('logout').finally()
|
||||
return http.delete('/logout').then(async () => {
|
||||
await store.dispatch('logout')
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import domain from './domain'
|
||||
import config from './config'
|
||||
import auth from './auth'
|
||||
import user from './user'
|
||||
|
||||
export default {
|
||||
domain,
|
||||
config,
|
||||
auth
|
||||
auth,
|
||||
user
|
||||
}
|
23
frontend/src/api/user.js
Normal file
23
frontend/src/api/user.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import http from '@/lib/http'
|
||||
|
||||
const base_url = '/user'
|
||||
|
||||
const user = {
|
||||
get_list(params) {
|
||||
return http.get(base_url + 's', {params: params})
|
||||
},
|
||||
|
||||
get(id) {
|
||||
return http.get(base_url + '/' + id)
|
||||
},
|
||||
|
||||
save(id = null, data) {
|
||||
return http.post(base_url + (id ? '/' + id : ''), data)
|
||||
},
|
||||
|
||||
destroy(id) {
|
||||
return http.delete(base_url + '/' + id)
|
||||
}
|
||||
}
|
||||
|
||||
export default user
|
|
@ -148,3 +148,10 @@ button {
|
|||
.ant-card {
|
||||
box-shadow: 0 0 30px rgba(200, 200, 200, 0.25);
|
||||
}
|
||||
|
||||
.ant-collapse {
|
||||
background: #ffffff;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background: #28292c;
|
||||
}
|
||||
}
|
57
frontend/src/components/RichText/CodeBlockComponent.vue
Normal file
57
frontend/src/components/RichText/CodeBlockComponent.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<node-view-wrapper class="code-block">
|
||||
<select contenteditable="false" v-model="selectedLanguage">
|
||||
<option :value="null">
|
||||
auto
|
||||
</option>
|
||||
<option disabled>
|
||||
—
|
||||
</option>
|
||||
<option v-for="(language, index) in languages" :value="language" :key="index">
|
||||
{{ language }}
|
||||
</option>
|
||||
</select>
|
||||
<pre><node-view-content as="code" /></pre>
|
||||
</node-view-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NodeViewWrapper, NodeViewContent, nodeViewProps } from '@tiptap/vue-2'
|
||||
export default {
|
||||
components: {
|
||||
NodeViewWrapper,
|
||||
NodeViewContent,
|
||||
},
|
||||
|
||||
props: nodeViewProps,
|
||||
|
||||
data() {
|
||||
return {
|
||||
languages: this.extension.options.lowlight.listLanguages(),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedLanguage: {
|
||||
get() {
|
||||
return (this.node.attrs.language ? this.node.attrs.language.split('')[0] : null)
|
||||
},
|
||||
set(language) {
|
||||
this.updateAttributes({ language })
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.code-block {
|
||||
position: relative;
|
||||
|
||||
select {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
162
frontend/src/components/RichText/MenuBar.vue
Normal file
162
frontend/src/components/RichText/MenuBar.vue
Normal file
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div>
|
||||
<template v-for="(item, index) in items">
|
||||
<div class="divider" v-if="item.type === 'divider'" :key="index" />
|
||||
<menu-item v-else :key="index" v-bind="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItem from './MenuItem.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
editor: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
icon: 'bold',
|
||||
title: '加粗',
|
||||
action: () => this.editor.chain().focus().toggleBold().run(),
|
||||
isActive: () => this.editor.isActive('bold'),
|
||||
},
|
||||
{
|
||||
icon: 'italic',
|
||||
title: '斜体',
|
||||
action: () => this.editor.chain().focus().toggleItalic().run(),
|
||||
isActive: () => this.editor.isActive('italic'),
|
||||
},
|
||||
{
|
||||
icon: 'strikethrough',
|
||||
title: '删除线',
|
||||
action: () => this.editor.chain().focus().toggleStrike().run(),
|
||||
isActive: () => this.editor.isActive('strike'),
|
||||
},
|
||||
{
|
||||
icon: 'code-view',
|
||||
title: '行内代码',
|
||||
action: () => this.editor.chain().focus().toggleCode().run(),
|
||||
isActive: () => this.editor.isActive('code'),
|
||||
},
|
||||
{
|
||||
icon: 'mark-pen-line',
|
||||
title: '高亮',
|
||||
action: () => this.editor.chain().focus().toggleHighlight().run(),
|
||||
isActive: () => this.editor.isActive('highlight'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'h-1',
|
||||
title: '一级标题',
|
||||
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => this.editor.isActive('heading', { level: 1 }),
|
||||
},
|
||||
{
|
||||
icon: 'h-2',
|
||||
title: '二级标题',
|
||||
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => this.editor.isActive('heading', { level: 2 }),
|
||||
},
|
||||
{
|
||||
icon: 'paragraph',
|
||||
title: '段落',
|
||||
action: () => this.editor.chain().focus().setParagraph().run(),
|
||||
isActive: () => this.editor.isActive('paragraph'),
|
||||
},
|
||||
{
|
||||
icon: 'list-unordered',
|
||||
title: '无序列表',
|
||||
action: () => this.editor.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => this.editor.isActive('bulletList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-ordered',
|
||||
title: '有序列表',
|
||||
action: () => this.editor.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => this.editor.isActive('orderedList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-check-2',
|
||||
title: '任务列表',
|
||||
action: () => this.editor.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => this.editor.isActive('taskList'),
|
||||
},
|
||||
{
|
||||
icon: 'code-box-line',
|
||||
title: '代码块',
|
||||
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => this.editor.isActive('codeBlock'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'double-quotes-l',
|
||||
title: '引用',
|
||||
action: () => this.editor.chain().focus().toggleBlockquote().run(),
|
||||
isActive: () => this.editor.isActive('blockquote'),
|
||||
},
|
||||
{
|
||||
icon: 'separator',
|
||||
title: '水平分割线',
|
||||
action: () => this.editor.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'text-wrap',
|
||||
title: '换行',
|
||||
action: () => this.editor.chain().focus().setHardBreak().run(),
|
||||
},
|
||||
{
|
||||
icon: 'format-clear',
|
||||
title: '清除格式',
|
||||
action: () => this.editor.chain()
|
||||
.focus()
|
||||
.clearNodes()
|
||||
.unsetAllMarks()
|
||||
.run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-back-line',
|
||||
title: '撤回',
|
||||
action: () => this.editor.chain().focus().undo().run(),
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-forward-line',
|
||||
title: '重做',
|
||||
action: () => this.editor.chain().focus().redo().run(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.divider {
|
||||
width: 2px;
|
||||
height: 1.25rem;
|
||||
background-color: rgba(#999999, 0.1);
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.75rem;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
71
frontend/src/components/RichText/MenuItem.vue
Normal file
71
frontend/src/components/RichText/MenuItem.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ title }}
|
||||
</template>
|
||||
<button
|
||||
class="menu-item"
|
||||
:class="{ 'is-active': isActive ? isActive(): null }"
|
||||
@click="action"
|
||||
:title="title"
|
||||
>
|
||||
<svg class="remix">
|
||||
<use :xlink:href="require('remixicon/fonts/remixicon.symbol.svg') + `#ri-${icon}`" />
|
||||
</svg>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
action: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isActive: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.menu-item {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: #0D0D0D;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color: #bdbdbd;
|
||||
}
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
background-color: #1e1f20;
|
||||
}
|
||||
}
|
||||
</style>
|
15
frontend/src/components/RichText/RichText.vue
Normal file
15
frontend/src/components/RichText/RichText.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div class="ProseMirror" v-html="html"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RichText",
|
||||
props: ['html']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less">
|
||||
@import "style";
|
||||
</style>
|
151
frontend/src/components/RichText/RichTextEditor.vue
Normal file
151
frontend/src/components/RichText/RichTextEditor.vue
Normal file
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div class="editor" v-if="editor">
|
||||
<menu-bar class="editor__header" :editor="editor"/>
|
||||
<editor-content :editor="editor"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Editor, EditorContent, VueNodeViewRenderer} from '@tiptap/vue-2'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import CodeBlockComponent from './CodeBlockComponent'
|
||||
import MenuBar from './MenuBar.vue'
|
||||
|
||||
import lowlight from 'lowlight'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
MenuBar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'changeValue'
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
// HTML
|
||||
const isSame = this.editor.getHTML() === value
|
||||
|
||||
// JSON
|
||||
// const isSame = this.editor.getJSON().toString() === value.toString()
|
||||
|
||||
if (isSame) {
|
||||
return
|
||||
}
|
||||
this.editor.commands.setContent(this.value, false)
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
const that = this
|
||||
this.editor = new Editor({
|
||||
onUpdate({editor}) {
|
||||
that.$emit('changeValue', editor.getHTML())
|
||||
},
|
||||
content: '',
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
CharacterCount,
|
||||
Highlight,
|
||||
CodeBlockLowlight
|
||||
.extend({
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(CodeBlockComponent)
|
||||
},
|
||||
}).configure({lowlight}),
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor.commands.setContent(this.value, false)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ant-affix {
|
||||
z-index: 8 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0.75rem;
|
||||
@gray: rgba(0, 0, 0, 0.2);
|
||||
background-color: #FFFFFF;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@gray: #666666;
|
||||
border: 1px solid @gray;
|
||||
background-color: #28292c;
|
||||
&__header {
|
||||
border-bottom: 1px solid @gray;
|
||||
}
|
||||
}
|
||||
border: 1px solid @gray;
|
||||
line-height: 1.5!important;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.25rem;
|
||||
border-bottom: 1px solid @gray;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 1.25rem 1rem;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
@import "style";
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.editor .ProseMirror {
|
||||
height: 500px;
|
||||
overflow: scroll;
|
||||
padding: 15px;
|
||||
}
|
||||
</style>
|
||||
|
148
frontend/src/components/RichText/style.less
Normal file
148
frontend/src/components/RichText/style.less
Normal file
|
@ -0,0 +1,148 @@
|
|||
/* Basic editor styles */
|
||||
.ProseMirror-focused {
|
||||
outline: unset;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(#616161, 0.1);
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0D0D0D;
|
||||
color: #FFF;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #FAF594;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#0D0D0D, 0.1);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0D0D0D, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #F98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #FBBC88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #B9F18D;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #FAF594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70CFF8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
220
frontend/src/components/StdDataDisplay/StdCurd.vue
Normal file
220
frontend/src/components/StdDataDisplay/StdCurd.vue
Normal file
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<div class="std-curd">
|
||||
<a-card :title="title">
|
||||
<a v-if="!disable_add" slot="extra" @click="add">添加</a>
|
||||
<std-table
|
||||
ref="table"
|
||||
v-bind="this.$props"
|
||||
@clickEdit="edit"
|
||||
@selected="onSelect"
|
||||
:key="update"
|
||||
>
|
||||
<template v-slot:actions="slotProps">
|
||||
<slot name="actions" :actions="slotProps.record"/>
|
||||
</template>
|
||||
</std-table>
|
||||
</a-card>
|
||||
<a-modal
|
||||
class="std-curd-edit-modal"
|
||||
:mask="false"
|
||||
:title="data.id ? '编辑 ID: ' + data.id : '添加'"
|
||||
:visible="visible"
|
||||
cancel-text="关闭"
|
||||
ok-text="保存"
|
||||
@cancel="visible=false;error={}"
|
||||
@ok="ok"
|
||||
:width="600"
|
||||
destroyOnClose
|
||||
>
|
||||
<std-data-entry ref="std_data_entry" :data-list="editableColumns()" :data-source="data"
|
||||
:error="error">
|
||||
<div slot="supplement">
|
||||
<slot name="supplement"></slot>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
</std-data-entry>
|
||||
</a-modal>
|
||||
<footer-tool-bar v-if="batch_columns.length">
|
||||
<a-space>
|
||||
当前已选中{{ selected.length }}条数据
|
||||
<a-button :disabled="!selected.length"
|
||||
@click="selected=[];update++">清空选中
|
||||
</a-button>
|
||||
<a-button type="primary"
|
||||
:disabled="!selected.length"
|
||||
@click="visible_batch_edit=true" ghost>批量修改
|
||||
</a-button>
|
||||
</a-space>
|
||||
</footer-tool-bar>
|
||||
<a-modal
|
||||
:mask="false"
|
||||
title="批量修改"
|
||||
:visible="visible_batch_edit"
|
||||
cancel-text="取消"
|
||||
ok-text="保存"
|
||||
@cancel="visible_batch_edit=false"
|
||||
@ok="okBatchEdit"
|
||||
>
|
||||
留空则不修改
|
||||
<std-data-entry :data-list="batch_columns" :data-source="data"/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StdTable from './StdTable'
|
||||
import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
|
||||
import FooterToolBar from "@/components/FooterToolbar/FooterToolBar"
|
||||
|
||||
export default {
|
||||
name: 'StdCurd',
|
||||
components: {
|
||||
StdTable,
|
||||
StdDataEntry,
|
||||
FooterToolBar
|
||||
},
|
||||
props: {
|
||||
api: Object,
|
||||
columns: Array,
|
||||
title: {
|
||||
type: String,
|
||||
default: '列表'
|
||||
},
|
||||
data_key: {
|
||||
type: String,
|
||||
default: 'data'
|
||||
},
|
||||
disable_search: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disable_add: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
soft_delete: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
edit_text: String,
|
||||
deletable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
get_params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
visible_batch_edit: false,
|
||||
data: {
|
||||
id: null,
|
||||
},
|
||||
error: {},
|
||||
params: {},
|
||||
selected: [],
|
||||
batch_columns: this.batchColumns(),
|
||||
update: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelect(keys) {
|
||||
this.selected = keys
|
||||
},
|
||||
batchColumns() {
|
||||
return this.columns.filter((column) => {
|
||||
return column.batch
|
||||
&& column.edit && column.edit.type !== 'upload'
|
||||
&& column.edit.type !== 'transfer'
|
||||
})
|
||||
},
|
||||
okBatchEdit() {
|
||||
this.api.batchSave(this.selected, this.data)
|
||||
.then(() => {
|
||||
this.$message.success('批量修改成功')
|
||||
this.$refs.table.get_list()
|
||||
}).catch(e => {
|
||||
this.$message.error(e.message)
|
||||
})
|
||||
},
|
||||
editableColumns() {
|
||||
return this.columns.filter((c) => {
|
||||
return c.edit
|
||||
})
|
||||
},
|
||||
uploadColumns() {
|
||||
return this.columns.filter(c => {
|
||||
return c.edit && c.edit.type === 'upload'
|
||||
})
|
||||
},
|
||||
async add() {
|
||||
this.data = {
|
||||
id: null
|
||||
}
|
||||
this.visible = true
|
||||
},
|
||||
async do_upload() {
|
||||
const columns = await this.uploadColumns()
|
||||
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const refs = this.$refs.std_data_entry.$refs
|
||||
const t = refs['std_upload_' + columns[i].dataIndex][0]
|
||||
if (t) {
|
||||
await t.upload()
|
||||
}
|
||||
}
|
||||
},
|
||||
async ok() {
|
||||
this.error = {}
|
||||
if (this.data.id) {
|
||||
await this.do_upload()
|
||||
this.api.save((this.data.id ? this.data.id : null), this.data).then(r => {
|
||||
this.$message.success('保存成功')
|
||||
this.data = Object.assign(this.data, r)
|
||||
this.$refs.table.get_list()
|
||||
}).catch(error => {
|
||||
this.$message.error((error.message ? error.message : '保存失败'), 5)
|
||||
this.error = error.errors
|
||||
})
|
||||
|
||||
} else {
|
||||
this.api.save((this.data.id ? this.data.id : null), this.data).then(r => {
|
||||
this.$message.success('保存成功')
|
||||
this.data = this.extend(this.data, r)
|
||||
this.$nextTick().then(() => {
|
||||
this.do_upload()
|
||||
})
|
||||
this.$refs.table.get_list()
|
||||
}).catch(error => {
|
||||
this.$message.error((error.message ? error.message : '保存失败'), 5)
|
||||
this.error = error.errors
|
||||
})
|
||||
}
|
||||
},
|
||||
edit(id) {
|
||||
this.api.get(id).then(r => {
|
||||
this.data = r
|
||||
this.visible = true
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
this.$message.error('系统错误')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
|
@ -6,6 +6,7 @@
|
|||
:pageSize="pagination.per_page"
|
||||
:size="size"
|
||||
:total="pagination.total"
|
||||
:show-total="(total, range) => `当前显示${range[0]}-${range[1]}条数据,共${total}条数据`"
|
||||
class="pagination"
|
||||
@change="changePage"
|
||||
/>
|
||||
|
@ -30,9 +31,21 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="less">
|
||||
.ant-pagination-total-text {
|
||||
@media (max-width: 450px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.pagination {
|
||||
padding: 10px 0 0 0;
|
||||
float: right;
|
||||
@media (max-width: 450px) {
|
||||
float: unset;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
339
frontend/src/components/StdDataDisplay/StdTable.vue
Normal file
339
frontend/src/components/StdDataDisplay/StdTable.vue
Normal file
|
@ -0,0 +1,339 @@
|
|||
<template>
|
||||
<div class="std-table">
|
||||
<std-data-entry
|
||||
v-if="!disable_search"
|
||||
:data-list="searchColumns"
|
||||
v-model="params"
|
||||
layout="inline"
|
||||
>
|
||||
<div slot="action">
|
||||
<a-form-item :wrapper-col="{span:8}">
|
||||
<a-button type="primary" @click="$router.push({
|
||||
query: Object.assign({}, params),
|
||||
}).catch(() => {})">查询
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:8}">
|
||||
<a-button @click="reset_search">重置</a-button>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</std-data-entry>
|
||||
<div v-if="soft_delete" style="text-align: right">
|
||||
<a v-if="params['trashed']" href="javascript:;"
|
||||
@click="params['trashed']=false; get_list()">返回</a>
|
||||
<a v-else href="javascript:;" @click="params['trashed']=true; get_list()">回收站</a>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="pithyColumns"
|
||||
:customRow="row"
|
||||
:data-source="data_source"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:row-key="rowKey"
|
||||
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange,
|
||||
onSelect: onSelect, type: selectionType,}"
|
||||
@change="stdChange"
|
||||
:scroll="{ x: scrollX }"
|
||||
>
|
||||
<template
|
||||
v-for="c in pithyColumns"
|
||||
:slot="c.scopedSlots.customRender"
|
||||
slot-scope="text, record"
|
||||
>
|
||||
<div v-if="c.badge" :key="c.dataIndex">
|
||||
<a-badge v-if="text === true || text > 0" status="success"/>
|
||||
<a-badge v-else status="error"/>
|
||||
{{ c.mask ? c.mask[text] : text }}
|
||||
</div>
|
||||
<span v-else-if="c.datetime"
|
||||
:key="c.dataIndex">{{ text ? moment(text).format("yyyy-MM-DD HH:mm:ss") : '无' }}</span>
|
||||
<span v-else-if="c.date" :key="c.dataIndex">{{ text ? moment(text).format("yyyy-MM-DD") : '无' }}</span>
|
||||
<div v-else-if="c.click" :key="c.dataIndex">
|
||||
<a href="javascript:;"
|
||||
@click="handleClick(
|
||||
record[c.click.index?c.click.index:c.dataIndex],
|
||||
c.click.index?c.click.index:c.dataIndex,
|
||||
c.click.method, c.click.path)">
|
||||
{{ text != null ? text : c.default }}
|
||||
</a>
|
||||
</div>
|
||||
<span v-else :key="c.dataIndex">{{ text != null ? (c.mask ? c.mask[text] : text) : c.default }}</span>
|
||||
</template>
|
||||
<div class="std_action" v-if="!pithy" slot="action" slot-scope="text, record">
|
||||
<a v-if="editable" @click="$emit('clickEdit', record[rowKey], record)">
|
||||
<template v-if="edit_text">{{ edit_text }}</template>
|
||||
<template v-else>编辑</template>
|
||||
</a>
|
||||
<slot name="actions" :record="record" />
|
||||
<template v-if="deletable">
|
||||
<a-divider type="vertical"/>
|
||||
<a-popconfirm
|
||||
v-if="soft_delete&¶ms.trashed"
|
||||
cancelText="再想想"
|
||||
okText="是的" title="你确定要反删除?"
|
||||
@confirm="restore(record.id)">
|
||||
<a href="javascript:;">反删除</a>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm
|
||||
v-else
|
||||
cancelText="再想想"
|
||||
okText="是的" title="你确定要删除?"
|
||||
@confirm="destroy(record.id)"
|
||||
>
|
||||
<a href="javascript:;">删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</div>
|
||||
</a-table>
|
||||
<std-pagination :pagination="pagination" @changePage="get_list"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StdPagination from './StdPagination'
|
||||
import moment from "moment"
|
||||
import StdDataEntry from "@/components/StdDataEntry/StdDataEntry"
|
||||
|
||||
export default {
|
||||
name: 'StdTable',
|
||||
components: {
|
||||
StdDataEntry,
|
||||
StdPagination,
|
||||
},
|
||||
props: {
|
||||
columns: Array,
|
||||
api: Object,
|
||||
data_key: String,
|
||||
selectionType: {
|
||||
type: String,
|
||||
default: 'checkbox',
|
||||
validator: function (value) {
|
||||
return ['checkbox', 'radio'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
pithy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disable_search: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
soft_delete: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
edit_text: String,
|
||||
deletable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
get_params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
scrollX: {
|
||||
type: [Number, Boolean],
|
||||
default: true
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
moment,
|
||||
data_source: [],
|
||||
loading: true,
|
||||
pagination: {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
current_page: 1,
|
||||
total_pages: 1
|
||||
},
|
||||
params: {
|
||||
...this.$route.query,
|
||||
...this.get_params
|
||||
},
|
||||
selectedRowKeys: [],
|
||||
rowSelection: {},
|
||||
searchColumns: this.get_searchColumns(),
|
||||
pithyColumns: this.get_pithyColumns(),
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.get_list()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.get_list()
|
||||
},
|
||||
methods: {
|
||||
get_list(page_num = null) {
|
||||
this.loading = true
|
||||
if (page_num) {
|
||||
this.params['page'] = page_num
|
||||
}
|
||||
this.api.get_list(this.params).then(response => {
|
||||
if (response[this.data_key] === undefined && response.data !== undefined) {
|
||||
this.data_source = response.data
|
||||
} else {
|
||||
this.data_source = response[this.data_key]
|
||||
}
|
||||
if (response.pagination !== undefined) {
|
||||
this.pagination = response.pagination
|
||||
}
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
this.$message.error('系统错误')
|
||||
})
|
||||
},
|
||||
stdChange(pagination, filters, sorter) {
|
||||
if (sorter) {
|
||||
this.params['order_by'] = sorter.field
|
||||
this.params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
|
||||
this.$nextTick(() => {
|
||||
this.get_list()
|
||||
})
|
||||
}
|
||||
},
|
||||
destroy(id) {
|
||||
this.api.destroy(id).then(() => {
|
||||
this.get_list()
|
||||
this.$message.success('删除 ID: ' + id + ' 成功')
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
if (e.message) {
|
||||
this.$message.error('错误 ' + e.message)
|
||||
} else {
|
||||
this.$message.error('系统错误')
|
||||
}
|
||||
})
|
||||
},
|
||||
restore(id) {
|
||||
this.api.restore(id).then(() => {
|
||||
this.get_list()
|
||||
this.$message.success('反删除 ID: ' + id + ' 成功')
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
if (e.message) {
|
||||
this.$message.error('错误' + e.message)
|
||||
} else {
|
||||
this.$message.error('系统错误')
|
||||
}
|
||||
})
|
||||
},
|
||||
get_searchColumns() {
|
||||
let searchColumns = []
|
||||
this.columns.forEach(column => {
|
||||
if (column.search) {
|
||||
if (column.edit && column.edit.type !== 'upload'
|
||||
&& column.edit.type !== 'transfer') {
|
||||
const tmp = Object.assign({}, column)
|
||||
tmp.edit = Object.assign({}, column.edit)
|
||||
if (typeof column.search === "string") {
|
||||
tmp.edit.type = column.search
|
||||
} else if (typeof column.search === "object") {
|
||||
tmp.edit = column.search
|
||||
}
|
||||
searchColumns.push(tmp)
|
||||
}
|
||||
// search 覆盖 edit
|
||||
if (!column.edit) {
|
||||
const tmp = Object.assign({}, column)
|
||||
tmp.edit = Object.assign({}, column.edit)
|
||||
if (typeof column.search === "object") {
|
||||
tmp.edit = column.search
|
||||
}
|
||||
searchColumns.push(tmp)
|
||||
}
|
||||
}
|
||||
})
|
||||
return searchColumns
|
||||
},
|
||||
get_pithyColumns() {
|
||||
if (this.pithy) {
|
||||
return this.columns.filter((c, index, columns) => {
|
||||
let display = c.pithy === true && c.display !== false
|
||||
columns[index]['scopedSlots'] = {}
|
||||
columns[index]['scopedSlots']['customRender'] =
|
||||
c.dataIndex !== 'title' ? c.dataIndex : '_' + c.dataIndex
|
||||
return display
|
||||
})
|
||||
}
|
||||
return this.columns.filter((c, index, columns) => {
|
||||
let display = c.display !== false
|
||||
columns[index]['scopedSlots'] = {}
|
||||
columns[index]['scopedSlots']['customRender'] =
|
||||
c.dataIndex !== 'title' ? c.dataIndex : '_' + c.dataIndex
|
||||
return display
|
||||
})
|
||||
},
|
||||
checked(c) {
|
||||
this.params[c.target.value] = c.target.checked
|
||||
},
|
||||
onSelectChange(selectedRowKeys) {
|
||||
this.selectedRowKeys = selectedRowKeys
|
||||
this.$emit('selected', selectedRowKeys)
|
||||
},
|
||||
onSelect(record) {
|
||||
this.$emit('selectedRecord', record)
|
||||
},
|
||||
handleClick(data, index, method = '', path = '') {
|
||||
if (method === 'router') {
|
||||
this.$router.push(path + '/' + data).then()
|
||||
} else {
|
||||
this.params[index] = data
|
||||
this.get_list()
|
||||
}
|
||||
},
|
||||
row(record) {
|
||||
return {
|
||||
on: {
|
||||
click: () => {
|
||||
this.$emit('clickRow', record.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async reset_search() {
|
||||
this.params = {}
|
||||
await this.$router.push({query: {}}).catch(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ant-table-scroll {
|
||||
.ant-table-body {
|
||||
overflow-x: auto!important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ant-form {
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.ant-slider {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.std-table {
|
||||
.ant-table-wrapper {
|
||||
// overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
61
frontend/src/components/StdDataEntry/StdCheckTag.vue
Normal file
61
frontend/src/components/StdDataEntry/StdCheckTag.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div>
|
||||
<template v-for="(v,k) in options">
|
||||
<a-checkable-tag
|
||||
:key="k"
|
||||
:checked="selectedTag === k"
|
||||
@change="() => handleChange(k)"
|
||||
>
|
||||
{{ v }}
|
||||
</a-checkable-tag>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StdCheckTag",
|
||||
data() {
|
||||
return {
|
||||
selectedTag: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: [Number, String, Boolean],
|
||||
options: [Array, Object],
|
||||
keyType: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'int'
|
||||
}
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
methods: {
|
||||
handleChange(tag) {
|
||||
this.selectedTag = tag
|
||||
this.$emit('change', isNaN(parseInt(tag)) || this.keyType === 'string' ? tag : parseInt(tag))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.selectedTag = this.value != null ? this.value.toString() : null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.selectedTag = this.value != null ? this.value.toString() : null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ant-tag {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.ant-tag-checkable-checked {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
:validate-status="error[d.dataIndex] ? 'error' :'success'"
|
||||
:wrapperCol="d.edit.wrapperCol"
|
||||
>
|
||||
<a-input v-if="d.edit.type==='input'" v-model="dataSource[d.dataIndex]" :placeholder="d.edit.placeholder"/>
|
||||
<a-input v-if="d.edit.type==='input'" v-model="dataSource[d.dataIndex]" :placeholder="d.edit.placeholder" />
|
||||
<a-textarea v-else-if="d.edit.type==='textarea'" v-model="dataSource[d.dataIndex]"
|
||||
:rows="d.edit.row?d.edit.row:5"/>
|
||||
<std-select-option
|
||||
|
@ -15,6 +15,13 @@
|
|||
v-model="temp[d.dataIndex]"
|
||||
:options="d.mask"
|
||||
:key-type="d.edit.key_type ? d.edit.key_type : 'int'"
|
||||
style="min-width: 120px"
|
||||
/>
|
||||
|
||||
<std-check-tag
|
||||
v-else-if="d.edit.type==='check-tag'"
|
||||
v-model="temp[d.dataIndex]"
|
||||
:options="d.mask"
|
||||
/>
|
||||
|
||||
<std-selector
|
||||
|
@ -22,12 +29,24 @@
|
|||
:columns="d.edit.columns"
|
||||
:data_key="d.edit.data_key"
|
||||
:disable_search="d.edit.disable_search" :pagination_method="d.edit.pagination_method"
|
||||
:record-value-index="d.edit.recordValueIndex" :value="temp[d.edit.valueIndex]"
|
||||
:record-value-index="d.edit.recordValueIndex" :value="fn(temp, d.edit.valueIndex)"
|
||||
:get_params="{...d.edit.get_params, ...bindModel(d.edit.bind, temp)}"
|
||||
selection-type="radio"
|
||||
/>
|
||||
|
||||
<a-input-number v-else-if="d.edit.type==='number'" v-model="temp[d.dataIndex]"
|
||||
:min="d.edit.min" :step="d.edit.step"
|
||||
:min="d.edit.min" :step="d.edit.step" :max="d.edit.max"
|
||||
/>
|
||||
|
||||
<std-upload v-else-if="d.edit.type==='upload'" :id="temp.id?temp.id:null" :ref="'std_upload_'+d.dataIndex"
|
||||
v-model="temp[d.dataIndex]" :api="d.edit.api"
|
||||
:api_delete="d.edit.api_delete"
|
||||
:list="temp[d.dataIndex]"
|
||||
: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)}"
|
||||
/>
|
||||
|
||||
<std-date-picker v-else-if="d.edit.type==='date_picker'" v-model="temp[d.dataIndex]"
|
||||
|
@ -44,9 +63,15 @@
|
|||
<a-switch
|
||||
v-else-if="d.edit.type==='switch'"
|
||||
v-model="temp[d.dataIndex]"
|
||||
@change="$emit(d.edit.event)"
|
||||
/>
|
||||
|
||||
<a-checkbox
|
||||
v-else-if="d.edit.type==='checkbox'"
|
||||
v-model="temp[d.dataIndex]"
|
||||
>
|
||||
{{ d.text }}
|
||||
</a-checkbox>
|
||||
|
||||
<std-transfer
|
||||
v-else-if="d.edit.type==='transfer'"
|
||||
v-model="temp[d.dataIndex]"
|
||||
|
@ -54,12 +79,17 @@
|
|||
:data-key="d.edit.dataKey"
|
||||
/>
|
||||
|
||||
<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[temp[d.dataIndex]] : temp[d.dataIndex] }}
|
||||
{{ d.mask ? d.mask[fn(temp, d.dataIndex)] : fn(temp, d.dataIndex) }}
|
||||
</p>
|
||||
|
||||
<p v-else>{{ "edit.type 参数非法 " + d.edit.type }}</p>
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<slot name="supplement"/>
|
||||
<slot name="action"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
@ -68,16 +98,22 @@
|
|||
<script>
|
||||
import StdSelectOption from './StdSelectOption'
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: 'StdDataEntry',
|
||||
components: {
|
||||
StdCheckTag,
|
||||
RichTextEditor,
|
||||
StdTransfer,
|
||||
StdDatePicker,
|
||||
StdSelectOption,
|
||||
StdSelector
|
||||
StdSelector,
|
||||
StdUpload
|
||||
},
|
||||
props: {
|
||||
dataList: [Array, Object],
|
||||
|
@ -123,6 +159,17 @@ export default {
|
|||
this.M_dataList = this.editableColumns(this.dataList)
|
||||
},
|
||||
methods: {
|
||||
fn: (obj, desc) => {
|
||||
const arr = desc.split('.')
|
||||
while (arr.length) {
|
||||
const top = obj[arr.shift()]
|
||||
if (!top) {
|
||||
return null
|
||||
}
|
||||
obj = top
|
||||
}
|
||||
return obj
|
||||
},
|
||||
editableColumns(columns) {
|
||||
if (typeof columns === 'object') {
|
||||
columns = Object.values(columns)
|
||||
|
@ -130,6 +177,15 @@ export default {
|
|||
return columns.filter((c) => {
|
||||
return c.edit
|
||||
})
|
||||
},
|
||||
bindModel(bind, dataSource) {
|
||||
let object = {}
|
||||
if (bind) {
|
||||
for (const [key, value] of Object.entries(bind)) {
|
||||
object[key] = this.fn(dataSource, value)
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,14 @@
|
|||
<a-date-picker
|
||||
v-model="dateModel"
|
||||
:show-time="showTime"
|
||||
@change="(r, d) => {changeDate(d)}"
|
||||
@change="r => {changeDate(r.format())}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const moment = require('moment')
|
||||
import moment from 'moment'
|
||||
import 'moment/locale/zh-cn'
|
||||
|
||||
moment.locale('zh-cn')
|
||||
|
||||
export default {
|
129
frontend/src/components/StdDataEntry/StdMultiFilesUpload.vue
Normal file
129
frontend/src/components/StdDataEntry/StdMultiFilesUpload.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-upload
|
||||
:before-upload="beforeUpload"
|
||||
:multiple="true"
|
||||
:show-upload-list="true"
|
||||
:file-list="uploadList"
|
||||
:remove="remove"
|
||||
>
|
||||
<a-button :disabled="disabled"><a-icon type="upload"/>选择文件</a-button>
|
||||
</a-upload>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="uploadList.length === 0 && !id"
|
||||
:loading="uploading"
|
||||
style="margin: 16px 0"
|
||||
@click="upload"
|
||||
v-if="id"
|
||||
>
|
||||
{{ uploading ? '上传中' : '开始上传' }}
|
||||
</a-button>
|
||||
<p style="margin: 15px 0" v-for="file in uploaded" :key="file.id">
|
||||
<a-icon type="paper-clip" style="margin-right: 5px"/>
|
||||
<a :href="server + '/' + file.path" target="_blank" @click="()=>{}">{{ getFileName(file.path) }}</a>
|
||||
<a-popconfirm
|
||||
title="确定要删除文件吗"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="deleteFile(file.id)"
|
||||
style="float: right"
|
||||
>
|
||||
<a-button type="link">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StdMultiFilesUpload",
|
||||
props: {
|
||||
api: Function,
|
||||
id: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
fileList: {
|
||||
default: null
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
api_delete: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fileList() {
|
||||
this.uploaded = this.fileList
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploadList: [],
|
||||
uploaded: this.fileList,
|
||||
lastFileTime: 0,
|
||||
server: process.env["VUE_APP_API_UPLOAD_ROOT"],
|
||||
uploading: false,
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: 'fileUrl',
|
||||
event: 'changeFileUrl'
|
||||
},
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.uploadList.length) {
|
||||
this.uploading = true
|
||||
let formData = new FormData()
|
||||
while (this.uploadList.length) {
|
||||
formData.append('file[]', this.uploadList.shift())
|
||||
}
|
||||
this.visible = false
|
||||
this.uploading = true
|
||||
this.$message.info('正在上传附件, 请不要关闭本页')
|
||||
return this.api(this.id, formData).then(r => {
|
||||
this.uploaded = [...this.uploaded, ...r]
|
||||
this.uploading = false
|
||||
this.$emit('uploaded', r)
|
||||
this.uploading = false
|
||||
this.orig = false
|
||||
this.$message.success('上传成功')
|
||||
}).catch(e => {
|
||||
this.$message.error(e.message ? e.message : '上传失败')
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUpload(file) {
|
||||
this.uploadList.push(file)
|
||||
return false
|
||||
},
|
||||
deleteFile(file_id) {
|
||||
this.api_delete(this.id, file_id).then(r => {
|
||||
this.uploaded = r
|
||||
})
|
||||
},
|
||||
getFileName(path) {
|
||||
// 从15开始找
|
||||
const idx = path.indexOf("/", 15)
|
||||
return path.substring(idx + 1)
|
||||
},
|
||||
remove(r) {
|
||||
this.uploadList = this.uploadList.filter(value => {
|
||||
return value !== r
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -11,7 +11,7 @@
|
|||
export default {
|
||||
name: 'StdSelectOption',
|
||||
props: {
|
||||
value: [Number, String],
|
||||
value: [Number, String, Boolean],
|
||||
options: [Array, Object],
|
||||
keyType: {
|
||||
type: String,
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="std-selector">
|
||||
<div class="std-selector" @click="visible=true">
|
||||
<a-input v-model="_key" disabled hidden/>
|
||||
<a-input v-model="M_value" disabled/>
|
||||
<a-button @click="visible=true">更变</a-button>
|
||||
<a-input
|
||||
v-model="M_value"
|
||||
disabled
|
||||
/>
|
||||
<a-modal
|
||||
:mask="false"
|
||||
:visible="visible"
|
||||
|
@ -11,14 +13,16 @@
|
|||
title="选择器"
|
||||
@cancel="visible=false"
|
||||
@ok="ok()"
|
||||
:width="600"
|
||||
destroyOnClose
|
||||
>
|
||||
<std-table
|
||||
:api="api"
|
||||
:columns="columns"
|
||||
:data_key="data_key"
|
||||
:disable_search="disable_search"
|
||||
:pagination_method="pagination_method"
|
||||
:pithy="true"
|
||||
:get_params="get_params"
|
||||
:selectionType="selectionType"
|
||||
@selected="onSelect"
|
||||
@selectedRecord="r => {record = r}"
|
||||
|
@ -28,11 +32,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import StdTable from '@/components/StdDataDisplay/StdTable'
|
||||
|
||||
export default {
|
||||
name: 'StdSelector',
|
||||
components: {StdTable},
|
||||
components: {
|
||||
StdTable: () => import('@/components/StdDataDisplay/StdTable')
|
||||
},
|
||||
props: {
|
||||
_key: [Number, String],
|
||||
value: String,
|
||||
|
@ -47,15 +52,15 @@ export default {
|
|||
api: Object,
|
||||
columns: Array,
|
||||
data_key: String,
|
||||
pagination_method: {
|
||||
type: String,
|
||||
validator: function (value) {
|
||||
return ['a', 'b'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
disable_search: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
get_params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
model: {
|
||||
|
@ -71,6 +76,11 @@ export default {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
_key() {
|
||||
if (!this._key) {
|
||||
this.M_value = null
|
||||
}
|
||||
},
|
||||
value() {
|
||||
this.M_value = this.value
|
||||
}
|
||||
|
@ -94,11 +104,13 @@ export default {
|
|||
|
||||
<style lang="less" scoped>
|
||||
.std-selector {
|
||||
min-width: 300px;
|
||||
|
||||
.ant-input {
|
||||
width: auto;
|
||||
margin: 0 10px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ant-input-disabled {
|
||||
background: unset;
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
83
frontend/src/components/StdDataEntry/StdSingleFileUpload.vue
Normal file
83
frontend/src/components/StdDataEntry/StdSingleFileUpload.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-upload
|
||||
:before-upload="beforeUpload"
|
||||
:multiple="false"
|
||||
:show-upload-list="true"
|
||||
:file-list="uploadList"
|
||||
>
|
||||
<a-button :disabled="disabled"><a-icon type="upload"/>上传</a-button>
|
||||
</a-upload>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StdSingleFileUpload",
|
||||
props: {
|
||||
api: Function,
|
||||
id: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
fileUrl: {
|
||||
default: null
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploadList: [],
|
||||
server: process.env["VUE_APP_API_UPLOAD_ROOT"],
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: 'fileUrl',
|
||||
event: 'changeFileUrl'
|
||||
},
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.uploadList.length) {
|
||||
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 => {
|
||||
this.$emit('uploaded', r.url)
|
||||
this.$emit('changeFileUrl', r.url)
|
||||
this.uploading = false
|
||||
this.$message.success('上传成功')
|
||||
}).catch(e => {
|
||||
this.$message.error(e.message ? e.message : '上传失败')
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUpload(file) {
|
||||
this.uploadList = [file]
|
||||
this.$emit('changeFileUrl', file.name)
|
||||
// 有自动上传参数就自动上传,没有就看 id, 没有 id 就不上传
|
||||
if (this.autoUpload ? this.autoUpload : (!!this.id)) {
|
||||
this.upload()
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
260
frontend/src/components/StdDataEntry/StdUpload.vue
Normal file
260
frontend/src/components/StdDataEntry/StdUpload.vue
Normal file
|
@ -0,0 +1,260 @@
|
|||
<template>
|
||||
<div v-if="type==='img'">
|
||||
<a-upload
|
||||
:before-upload="beforeUpload"
|
||||
:show-upload-list="false"
|
||||
class="avatar-uploader"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<img v-if="fileUrl" :src="getFileUrl()" width="100">
|
||||
<div v-else>
|
||||
<a-icon :type="uploading ? 'loading' : 'plus'"/>
|
||||
<div class="ant-upload-text">
|
||||
上传图片
|
||||
</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
|
||||
<a-modal
|
||||
v-if="crop"
|
||||
v-model="visible"
|
||||
cancelText="取消上传"
|
||||
class="cropper"
|
||||
okText="裁切"
|
||||
title="图片裁切"
|
||||
@cancel="visible=false;$emit('changeFileUrl', orig)"
|
||||
@ok="handleCropSuccess"
|
||||
>
|
||||
<div class="vue-cropper" v-if="fileUrl.substring(0,5) === 'data:'">
|
||||
<VueCropper
|
||||
ref="cropper"
|
||||
:autoCrop="true"
|
||||
:autoCropHeight="cropOptions.autoCropHeight"
|
||||
:autoCropWidth="cropOptions.autoCropWidth"
|
||||
:fixed="cropOptions.fixed"
|
||||
:fixedNumber="cropOptions.fixedNumber"
|
||||
:img="getFileUrl()"
|
||||
outputType="png"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin: 10px 0">
|
||||
<a-button @click="handleSingleUpload">不剪裁</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<div v-else-if="type==='file'">
|
||||
<std-single-file-upload
|
||||
:file-url="fileUrl"
|
||||
:id="id"
|
||||
:api="api"
|
||||
:auto-upload="autoUpload"
|
||||
@changeFileUrl="url => {$emit('changeFileUrl', url)}"
|
||||
:disabled="disabled"
|
||||
ref="single-file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="type==='multi-file'">
|
||||
<std-multi-files-upload
|
||||
:file-list="M_list"
|
||||
:id="id"
|
||||
:api="api"
|
||||
:auto-upload="autoUpload"
|
||||
:api_delete="api_delete"
|
||||
@changeFileUrl="url => {$emit('changeFileUrl', url)}"
|
||||
:disabled="disabled"
|
||||
ref="multi-file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
Vue.use(VueCropper)
|
||||
|
||||
export default {
|
||||
name: 'StdUpload',
|
||||
components: {StdMultiFilesUpload, StdSingleFileUpload},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
api: Function,
|
||||
api_delete: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
fileUrl: {
|
||||
default: ''
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
default: 'img',
|
||||
validator: value => {
|
||||
return ['img', 'file', 'multi-file'].indexOf(value) !== -1
|
||||
}
|
||||
},
|
||||
crop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cropOptions: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
fixed: true,
|
||||
autoCropWidth: 200,
|
||||
autoCropHeight: 200,
|
||||
}
|
||||
}
|
||||
},
|
||||
list: {
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploading: false,
|
||||
orig: '',
|
||||
visible: false,
|
||||
fileList: [],
|
||||
M_list: this.list,
|
||||
server: process.env["VUE_APP_API_UPLOAD_ROOT"]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.orig = this.fileUrl
|
||||
},
|
||||
model: {
|
||||
prop: 'fileUrl',
|
||||
event: 'changeFileUrl'
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.M_list = this.list
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFileUrl() {
|
||||
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()
|
||||
}
|
||||
if (this.orig && this.fileUrl !== this.orig) {
|
||||
return this.handleSingleUpload()
|
||||
}
|
||||
if (this.$refs['single-file']) {
|
||||
return await this.$refs["single-file"].upload()
|
||||
}
|
||||
},
|
||||
handleSingleUpload() {
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.fileList[0])
|
||||
this.visible = false
|
||||
this.uploading = true
|
||||
this.$message.info('正在上传附件, 请不要关闭本页')
|
||||
|
||||
return this.api(this.id, formData).then(r => {
|
||||
this.$emit('uploaded', r.url)
|
||||
this.$emit('changeFileUrl', r.url)
|
||||
this.uploading = false
|
||||
this.$message.success('上传成功')
|
||||
this.orig = r.url
|
||||
})
|
||||
|
||||
},
|
||||
beforeUpload(file) {
|
||||
// 赋予新值之前做个备份 emm 生气了哼!!!
|
||||
this.orig = this.fileUrl ? this.fileUrl : 'orig_is_empty'
|
||||
this.fileList = [file]
|
||||
if (this.type === 'img') {
|
||||
this.visible = true
|
||||
const r = new FileReader()
|
||||
r.readAsDataURL(file)
|
||||
r.onload = e => {
|
||||
file.thumbUrl = e.target.result
|
||||
this.$emit('changeFileUrl', e.target.result)
|
||||
}
|
||||
} else {
|
||||
this.$emit('changeFileUrl', file.name)
|
||||
}
|
||||
return false
|
||||
},
|
||||
afterCropUpload(file) {
|
||||
this.visible = true
|
||||
const r = new FileReader()
|
||||
r.readAsDataURL(file)
|
||||
r.onload = e => {
|
||||
file.thumbUrl = e.target.result
|
||||
this.$emit('changeFileUrl', e.target.result)
|
||||
}
|
||||
this.fileList = [file]
|
||||
this.$nextTick(() => {
|
||||
this.handleSingleUpload()
|
||||
})
|
||||
},
|
||||
handleCropSuccess() {
|
||||
this.$refs.cropper.getCropBlob((data) => {
|
||||
let file = new window.File([data], uuidv4() + '.png', {type: data.type})
|
||||
this.afterCropUpload(file)
|
||||
this.visible = false
|
||||
})
|
||||
},
|
||||
remove(r) {
|
||||
this.fileList = this.fileList.filter(value => {
|
||||
return value !== r
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upload-picture-btn {
|
||||
font-size: 20px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.cropper {
|
||||
.ant-modal-body {
|
||||
min-height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-cropper {
|
||||
min-height: 200px;
|
||||
background-image: unset;
|
||||
}
|
||||
|
||||
.img-preview {
|
||||
float: left;
|
||||
border: 1px solid #8e8e904d;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
|
||||
img {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
52
frontend/src/components/VueItextarea/VueItextarea.vue
Normal file
52
frontend/src/components/VueItextarea/VueItextarea.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<codemirror v-model="current_value" :options="cmOptions"/>
|
||||
</template>
|
||||
<style lang="less">
|
||||
.cm-s-monokai {
|
||||
height: auto!important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
|
||||
import 'codemirror/mode/nginx/nginx'
|
||||
|
||||
export default {
|
||||
name: 'vue-itextarea',
|
||||
components: {
|
||||
codemirror
|
||||
},
|
||||
props: {
|
||||
value: {},
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'changeValue'
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.current_value = this.value ?? ''
|
||||
},
|
||||
current_value() {
|
||||
this.$emit('changeValue', this.current_value)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
current_value: this.value ?? '',
|
||||
cmOptions: {
|
||||
tabSize: 4,
|
||||
mode: 'text/x-nginx-conf',
|
||||
theme: 'monokai',
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
highlightDifferences: true,
|
||||
defaultTextHeight: 1000,
|
||||
// more CodeMirror options...
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
<a-icon type="menu-unfold" @click="$emit('clickUnFold')"/>
|
||||
</div>
|
||||
<div class="user-wrapper">
|
||||
<a href="/index.html">
|
||||
<a href="/">
|
||||
<a-icon type="home"/>
|
||||
</a>
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
:openKeys="openKeys"
|
||||
mode="inline"
|
||||
@openChange="onOpenChange"
|
||||
:default-selected-keys="[$route.path.substring(1)]"
|
||||
v-model="selectedKey"
|
||||
>
|
||||
<template v-for="sidebar in visible(sidebars)">
|
||||
<a-menu-item v-if="!sidebar.children" :key="sidebar.path"
|
||||
<a-menu-item v-if="!sidebar.children" :key="sidebar.name"
|
||||
@click="$router.push('/'+sidebar.path).catch(() => {})">
|
||||
<a-icon :type="sidebar.meta.icon"/>
|
||||
<span>{{ sidebar.name }}</span>
|
||||
|
@ -36,8 +36,17 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
rootSubmenuKeys: [],
|
||||
openKeys: [],
|
||||
sidebars: this.$routeConfig[0]['children']
|
||||
openKeys: [this.openSub()],
|
||||
sidebars: this.$routeConfig[0]['children'],
|
||||
selectedKey: [this.$route.name],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route'() {
|
||||
this.selectedKey = [this.$route.name]
|
||||
const sub = this.openSub()
|
||||
const p = this.openKeys.indexOf(sub)
|
||||
if (p === -1) this.openKeys.push(this.openSub())
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -46,6 +55,11 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
openSub() {
|
||||
let path = this.$route.path
|
||||
let lastSepIndex = path.lastIndexOf('/')
|
||||
return path.substring(1, lastSepIndex)
|
||||
},
|
||||
onOpenChange(openKeys) {
|
||||
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
|
||||
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
|
|
@ -40,7 +40,6 @@ http.interceptors.response.use(
|
|||
case 403:
|
||||
// 无权访问时,直接登出
|
||||
await store.dispatch('logout')
|
||||
location.reload()
|
||||
break
|
||||
}
|
||||
return Promise.reject(error.response.data)
|
|
@ -32,12 +32,9 @@ export default {
|
|||
Vue.prototype.scrollPosition = scrollPosition
|
||||
|
||||
Vue.prototype.getWebSocketRoot = () => {
|
||||
const protocol = location.protocol === "https:" ? "wss://" : "ws://"
|
||||
if (process.env["VUE_APP_API_WSS_ROOT"]) {
|
||||
return process.env["VUE_APP_API_WSS_ROOT"]
|
||||
}
|
||||
console.log(protocol, document.domain)
|
||||
return protocol + document.domain + '/ws'
|
||||
const protocol = location.protocol === "https:" ? "wss://" : "ws://"
|
||||
|
||||
return protocol + location.host + process.env["VUE_APP_API_WSS_ROOT"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,14 @@ export const routes = [
|
|||
icon: 'home'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: '用户管理',
|
||||
component: () => import('@/views/User.vue'),
|
||||
meta: {
|
||||
icon: 'user'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'domain',
|
||||
name: '网站管理',
|
||||
|
@ -93,11 +101,12 @@ export const routes = [
|
|||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
routes,
|
||||
mode: 'history'
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = 'Nginx UI | ' + to.name
|
||||
document.title = to.name + ' | Nginx UI'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
axios.get('/version.json?' + Date.now()).then(r => {
|
|
@ -24,7 +24,7 @@ export default {
|
|||
return {
|
||||
this_year: date.getFullYear(),
|
||||
version: process.env.VUE_APP_VERSION,
|
||||
build_id: process.env.VUE_APP_BUILD_ID ? process.env.VUE_APP_BUILD_ID : '开发模式',
|
||||
build_id: process.env.VUE_APP_TOTAL_BUILD ?? '开发模式',
|
||||
api_root: process.env.VUE_APP_API_ROOT
|
||||
}
|
||||
},
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue