mirror of
https://github.com/jumpserver/lina.git
synced 2026-01-14 03:46:26 +00:00
Compare commits
422 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48beb502bd | ||
|
|
383a799c6a | ||
|
|
c378b2bf0d | ||
|
|
95ccc5bc16 | ||
|
|
dc112bfee8 | ||
|
|
84930b27fa | ||
|
|
2bf1496480 | ||
|
|
27a78c3b23 | ||
|
|
95007a52a5 | ||
|
|
50850af382 | ||
|
|
92097e11e5 | ||
|
|
02e1299cbd | ||
|
|
bccaaac1c7 | ||
|
|
3b91b60bc2 | ||
|
|
dca0753492 | ||
|
|
d1920595d8 | ||
|
|
97638bc7a3 | ||
|
|
f8ab8035e5 | ||
|
|
39c1aa1e7e | ||
|
|
6f07c36393 | ||
|
|
b700d0fc83 | ||
|
|
2d7fd30da1 | ||
|
|
99b852d552 | ||
|
|
37234f0e20 | ||
|
|
da87848f44 | ||
|
|
2aa363f6ce | ||
|
|
97a98ace03 | ||
|
|
f53be25e19 | ||
|
|
3c36b50c35 | ||
|
|
eb75122a76 | ||
|
|
9f3a1e082b | ||
|
|
ca8307338f | ||
|
|
ee53941fb0 | ||
|
|
6d7fec5bf9 | ||
|
|
d31492f43b | ||
|
|
5056c8cbf9 | ||
|
|
00cf2c34b9 | ||
|
|
c15f22e05b | ||
|
|
283dfc97ec | ||
|
|
a13c009b37 | ||
|
|
2494583098 | ||
|
|
2bb1fce491 | ||
|
|
49e2c547e7 | ||
|
|
0bac33c9fc | ||
|
|
1b3f5403dd | ||
|
|
b4fd09e308 | ||
|
|
96b002eccb | ||
|
|
15e253804e | ||
|
|
8427cb4e22 | ||
|
|
c8c6cbc3b1 | ||
|
|
ef841149de | ||
|
|
b51783d98f | ||
|
|
80ff4064e6 | ||
|
|
091038de95 | ||
|
|
cc994b8ecd | ||
|
|
73d4ec0fa5 | ||
|
|
8ee39c036f | ||
|
|
e8335d9c73 | ||
|
|
f720eaf05a | ||
|
|
7f118d7074 | ||
|
|
a1ad901b40 | ||
|
|
27b39ad910 | ||
|
|
f053a37606 | ||
|
|
dcb94f4aac | ||
|
|
91cbe97a5b | ||
|
|
d3053fdf18 | ||
|
|
226f17c4fd | ||
|
|
34a30571c1 | ||
|
|
0a19829c4a | ||
|
|
09a252f282 | ||
|
|
c7c6f5ac82 | ||
|
|
f3b15727cb | ||
|
|
0eb8e1fab3 | ||
|
|
a80918139b | ||
|
|
67ffda2fd0 | ||
|
|
fa6831b743 | ||
|
|
8bd77fa6c9 | ||
|
|
6a45fccfcd | ||
|
|
c39733cf66 | ||
|
|
ea4a7f53e3 | ||
|
|
6639614caf | ||
|
|
f17ec57b3a | ||
|
|
b04cf80201 | ||
|
|
6dc71fe612 | ||
|
|
9cbec5e1ab | ||
|
|
703eadf292 | ||
|
|
d2aa4e99da | ||
|
|
4509970f7d | ||
|
|
b03af2c995 | ||
|
|
328e068aca | ||
|
|
65c6922621 | ||
|
|
ebb8af42f2 | ||
|
|
0b821ffc04 | ||
|
|
5b6e2970bf | ||
|
|
d01e991885 | ||
|
|
552b26e163 | ||
|
|
35814d3d5c | ||
|
|
31e46a3ede | ||
|
|
e7815f528c | ||
|
|
4d747686cd | ||
|
|
12697ab279 | ||
|
|
f4f4b7ccc1 | ||
|
|
41bbd4a70b | ||
|
|
a66403dcf0 | ||
|
|
31b75aa139 | ||
|
|
411aae6829 | ||
|
|
0ad025f63b | ||
|
|
312de6c6eb | ||
|
|
5b05bd12c6 | ||
|
|
a87ea94e78 | ||
|
|
9994020f97 | ||
|
|
fc0709f1ce | ||
|
|
9235d68825 | ||
|
|
4a17efb015 | ||
|
|
1cc69e4203 | ||
|
|
9de8d622e4 | ||
|
|
844dfb5ac1 | ||
|
|
9d3fd73367 | ||
|
|
c7731a1d2f | ||
|
|
bf161d688a | ||
|
|
ab2d5b3fed | ||
|
|
6a40ab1a4c | ||
|
|
4d11e638e8 | ||
|
|
97c4a397e2 | ||
|
|
9239d69c14 | ||
|
|
3ea68fe13f | ||
|
|
04ce056076 | ||
|
|
67c1c8db58 | ||
|
|
d8566d2f9e | ||
|
|
d74da503c8 | ||
|
|
e1d8e4aea6 | ||
|
|
d21559599f | ||
|
|
f8cadb545f | ||
|
|
2bc4b53159 | ||
|
|
7f28cc0aad | ||
|
|
7e95e38d24 | ||
|
|
57bafc01e3 | ||
|
|
834033f2fd | ||
|
|
9a41ccbdd7 | ||
|
|
3793370c9c | ||
|
|
4486dc55a7 | ||
|
|
bdb63b865a | ||
|
|
2d17b48b86 | ||
|
|
67091d5a22 | ||
|
|
798c4ca64e | ||
|
|
eddd27e95d | ||
|
|
12ffa363c1 | ||
|
|
cd79246f0d | ||
|
|
94583e2156 | ||
|
|
ca602a8052 | ||
|
|
5bdc4e4e3a | ||
|
|
da1b73d3fd | ||
|
|
4bc4012520 | ||
|
|
b8f1cb7a8e | ||
|
|
ee6a3c6d68 | ||
|
|
be176ad408 | ||
|
|
73c17fccbe | ||
|
|
30c1284a41 | ||
|
|
191900381a | ||
|
|
91e04a8d18 | ||
|
|
1b223f0486 | ||
|
|
22eb78339e | ||
|
|
5831cb326c | ||
|
|
52a4c1824f | ||
|
|
c155e5a59b | ||
|
|
ff90e56763 | ||
|
|
14000317b9 | ||
|
|
a24fab51af | ||
|
|
4184401432 | ||
|
|
dc59836a66 | ||
|
|
ba6433a585 | ||
|
|
b88c90bb75 | ||
|
|
4eb5155aed | ||
|
|
dd4aed9cf4 | ||
|
|
2274b65d83 | ||
|
|
e0a927c7e1 | ||
|
|
d294111e9e | ||
|
|
d6fef086c0 | ||
|
|
cd6418ef4b | ||
|
|
00909b364d | ||
|
|
cdd3df1562 | ||
|
|
081f43887b | ||
|
|
95ba08afa6 | ||
|
|
88aa17550b | ||
|
|
8c3337f581 | ||
|
|
6d0d9650b4 | ||
|
|
86285c12bd | ||
|
|
89b96a4542 | ||
|
|
707bf497b0 | ||
|
|
7a0ef53d78 | ||
|
|
2a0626e4f0 | ||
|
|
a760d0eeaa | ||
|
|
f7ccbde502 | ||
|
|
8b350ba819 | ||
|
|
f87d605e13 | ||
|
|
4a38cb7168 | ||
|
|
1ebb8dda0a | ||
|
|
2f9b33898f | ||
|
|
14b854a872 | ||
|
|
235131fd81 | ||
|
|
9b1ba09404 | ||
|
|
6fc8a43e34 | ||
|
|
324db2fdae | ||
|
|
3367363445 | ||
|
|
5f6846fa47 | ||
|
|
d15292ad0e | ||
|
|
65cd456ae9 | ||
|
|
1748ae760a | ||
|
|
7dc5ec8fa7 | ||
|
|
543e0f7aa7 | ||
|
|
4b152bf9bf | ||
|
|
0afc160b56 | ||
|
|
79c89676a6 | ||
|
|
78fa90c9f8 | ||
|
|
c1ed466b8b | ||
|
|
35b8181589 | ||
|
|
c172056998 | ||
|
|
752e3a7a28 | ||
|
|
4935c32bb9 | ||
|
|
7e184a4061 | ||
|
|
30a7063999 | ||
|
|
c94a451df9 | ||
|
|
f6aab29ecc | ||
|
|
3ad157848a | ||
|
|
e9c54d7eeb | ||
|
|
b8b19fed53 | ||
|
|
8dbc7a404f | ||
|
|
a9e95fd705 | ||
|
|
a127b872cc | ||
|
|
01aa92adc0 | ||
|
|
0d31318fd5 | ||
|
|
a595d28a5b | ||
|
|
5d973944ea | ||
|
|
1d008330a1 | ||
|
|
337ff47806 | ||
|
|
e453a9a740 | ||
|
|
e4ec8565f0 | ||
|
|
22904ba421 | ||
|
|
5371faf019 | ||
|
|
7c6a3340ad | ||
|
|
166e66ff9e | ||
|
|
1c39d33d43 | ||
|
|
159c6d8208 | ||
|
|
917d95cc7b | ||
|
|
6fde735cbd | ||
|
|
4ee32dd51b | ||
|
|
19dc6aa5a0 | ||
|
|
8c7f08a971 | ||
|
|
8df3841040 | ||
|
|
a4a14fecdd | ||
|
|
52616fead9 | ||
|
|
95d0afc5cb | ||
|
|
07c36e717e | ||
|
|
de393cd2b6 | ||
|
|
1203941e6b | ||
|
|
cfa8fcf352 | ||
|
|
8bfd6b8654 | ||
|
|
9840396a6f | ||
|
|
309d9379b9 | ||
|
|
a15ce0b77f | ||
|
|
06e80fe75f | ||
|
|
009be1be83 | ||
|
|
eb20b32fcf | ||
|
|
9b19d862f6 | ||
|
|
38b21357b7 | ||
|
|
c40bd0a9ab | ||
|
|
48a7310739 | ||
|
|
2c69b36291 | ||
|
|
d267cd1f5e | ||
|
|
807e3a407a | ||
|
|
1ba790e680 | ||
|
|
44b701edbc | ||
|
|
8619ab8bca | ||
|
|
79e92fa46b | ||
|
|
f19c863440 | ||
|
|
fff8b79a45 | ||
|
|
cf810b3d3e | ||
|
|
f58e37a76a | ||
|
|
5889e20aae | ||
|
|
7caa2c8264 | ||
|
|
865388dedc | ||
|
|
35c1077eed | ||
|
|
487e199995 | ||
|
|
f584e96675 | ||
|
|
1f4f1d3712 | ||
|
|
08facb1eda | ||
|
|
34cb9424d4 | ||
|
|
36767cd265 | ||
|
|
86b9fc8f5a | ||
|
|
49054e5dc0 | ||
|
|
bac7cef23c | ||
|
|
4013ea6212 | ||
|
|
37153ebe1d | ||
|
|
da35d9be25 | ||
|
|
c3c24b0ad1 | ||
|
|
98da517724 | ||
|
|
f002c7f917 | ||
|
|
0d4e4324ce | ||
|
|
fdeab46970 | ||
|
|
5acbdd5679 | ||
|
|
3a64120241 | ||
|
|
81d1cbf3a1 | ||
|
|
cec17bbef8 | ||
|
|
fbc3373e1b | ||
|
|
b4a935ab15 | ||
|
|
7fbff42067 | ||
|
|
5077fec5a8 | ||
|
|
c4619af96f | ||
|
|
025d0abeae | ||
|
|
5735a591ba | ||
|
|
3b664ee1dc | ||
|
|
a3f6de330e | ||
|
|
cbc67a5a4c | ||
|
|
dec0593907 | ||
|
|
1ed432b1e2 | ||
|
|
b65664f9c4 | ||
|
|
f64def0bec | ||
|
|
06f6202bc4 | ||
|
|
6fa7800d6b | ||
|
|
54aa252c20 | ||
|
|
c083f6c4a4 | ||
|
|
73bb854ebb | ||
|
|
d6f9df277e | ||
|
|
ba78e33f89 | ||
|
|
09ef15cff0 | ||
|
|
62d520e625 | ||
|
|
f07a857813 | ||
|
|
2699d5e8eb | ||
|
|
fb398ca3e4 | ||
|
|
3230c37318 | ||
|
|
bc258a7ff8 | ||
|
|
64d610e282 | ||
|
|
4d95461b5c | ||
|
|
f364c8fdf9 | ||
|
|
88dc2d9271 | ||
|
|
3aced25da4 | ||
|
|
fcf142b696 | ||
|
|
2123037897 | ||
|
|
f5bc2842ec | ||
|
|
2c95e5f10b | ||
|
|
b3ff9c5bcb | ||
|
|
905e5e00b1 | ||
|
|
81db3d86fa | ||
|
|
ea15515264 | ||
|
|
4048a000c7 | ||
|
|
a60693c41c | ||
|
|
06d6c54db8 | ||
|
|
57d5c893d3 | ||
|
|
435ce24c75 | ||
|
|
4a757bb6bc | ||
|
|
32fa4f0b11 | ||
|
|
9f12e1aa18 | ||
|
|
6165709747 | ||
|
|
0ad1eef196 | ||
|
|
d2d07555b5 | ||
|
|
7f60224c6d | ||
|
|
bbf502c85d | ||
|
|
e6aaa52506 | ||
|
|
70affacfde | ||
|
|
40a8da5e58 | ||
|
|
24266bb929 | ||
|
|
14286b961e | ||
|
|
0498db9a8f | ||
|
|
266d107ffd | ||
|
|
4f2a9c0c6c | ||
|
|
affb0ec2bb | ||
|
|
3075d50357 | ||
|
|
e49da02c4d | ||
|
|
7df5736354 | ||
|
|
98886149f9 | ||
|
|
abb98d55b9 | ||
|
|
f9c979af88 | ||
|
|
89018a2258 | ||
|
|
a0c29563ca | ||
|
|
21223fddea | ||
|
|
254a2b58cc | ||
|
|
777c371070 | ||
|
|
0cba2b3116 | ||
|
|
c27dd0baef | ||
|
|
990aebefdd | ||
|
|
6f84312dbe | ||
|
|
d4c12fb38f | ||
|
|
1bb94824df | ||
|
|
5772430761 | ||
|
|
790941f361 | ||
|
|
a9ce01ac0e | ||
|
|
1cdc406e70 | ||
|
|
cb60660272 | ||
|
|
8625e21077 | ||
|
|
2251a1653e | ||
|
|
ff1debcbce | ||
|
|
17e5564cd7 | ||
|
|
615576b3fd | ||
|
|
c0d3fbb47a | ||
|
|
09075b13b7 | ||
|
|
4e92c1a77c | ||
|
|
7cc49bc907 | ||
|
|
2e2b5bf873 | ||
|
|
66b1d17dd2 | ||
|
|
ee26e47c4d | ||
|
|
aedf6d2158 | ||
|
|
6e848e65b4 | ||
|
|
5c0108906c | ||
|
|
6c1f8ec8f7 | ||
|
|
dda36d2b40 | ||
|
|
1abf30c347 | ||
|
|
697b5a3d13 | ||
|
|
74e4c3397e | ||
|
|
c227bf59a6 | ||
|
|
0092f6d6d7 | ||
|
|
747477b27c | ||
|
|
4943dab50c | ||
|
|
5cac3ee1f7 | ||
|
|
fa5a227aff | ||
|
|
792e8595b8 | ||
|
|
9d62614ff4 | ||
|
|
48c0f6e8c6 | ||
|
|
31b17b384d | ||
|
|
858d7a9d6f | ||
|
|
48b6c48581 | ||
|
|
38be9dd367 | ||
|
|
7e5570ad72 |
@@ -46,7 +46,7 @@ server {
|
||||
|
||||
|
||||
## License & Copyright
|
||||
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
]
|
||||
}
|
||||
|
||||
10
nginx.conf
10
nginx.conf
@@ -1,6 +1,16 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
#gzip_http_version 1.0;
|
||||
gzip_comp_level 8;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
|
||||
gzip_vary off;
|
||||
gzip_static on;
|
||||
gzip_disable "MSIE [1-6].";
|
||||
|
||||
location /ui/ {
|
||||
try_files $uri / /ui/index.html;
|
||||
alias /opt/lina/;
|
||||
|
||||
13
package.json
13
package.json
@@ -15,17 +15,23 @@
|
||||
"test:ci": "npm run lint && npm run test:unit",
|
||||
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
|
||||
"vue-i18n-extract": "vue-i18n-extract",
|
||||
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
|
||||
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
|
||||
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
|
||||
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||
"@ztree/ztree_v3": "3.5.44",
|
||||
"axios": "0.18.1",
|
||||
"axios": "0.21.1",
|
||||
"axios-retry": "^3.1.9",
|
||||
"deepmerge": "^4.2.2",
|
||||
"echarts": "^4.7.0",
|
||||
"element-ui": "2.13.2",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"install": "^0.13.0",
|
||||
"jquery": "^3.5.0",
|
||||
"js-cookie": "2.2.0",
|
||||
"krry-transfer": "^1.7.3",
|
||||
"less": "^3.10.3",
|
||||
"less-loader": "^5.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -40,8 +46,10 @@
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"lodash.values": "^4.3.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-parseformat": "^3.0.0",
|
||||
"normalize.css": "7.0.0",
|
||||
"npm": "^7.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "2.4.0",
|
||||
"vue": "2.6.10",
|
||||
@@ -72,6 +80,7 @@
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "23.6.0",
|
||||
"chalk": "2.4.2",
|
||||
"compression-webpack-plugin": "^6.1.1",
|
||||
"connect": "3.6.6",
|
||||
"element-theme-chalk": "^2.13.1",
|
||||
"eslint": "^5.15.3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -8,7 +8,6 @@
|
||||
<meta http-equiv="Cache-control" content="no-cache">
|
||||
<meta http-equiv="Cache" content="no-cache">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
15
src/api/orgs.js
Normal file
15
src/api/orgs.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getOrgDetail(oid) {
|
||||
return request({
|
||||
url: `/api/v1/orgs/orgs/current/?oid=${oid}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCurrentOrg() {
|
||||
return request({
|
||||
url: `/api/v1/orgs/orgs/current/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import request from '@/utils/request'
|
||||
|
||||
export function getTicketOpenCount(assign) {
|
||||
return request({
|
||||
url: `/api/v1/tickets/tickets/?assign=${assign}&status=open&offset=0&limit=15&display=1&draw=1/`,
|
||||
url: `/api/v1/tickets/tickets/?assignees__id=${assign}&status=open&offset=0&limit=15&display=1&draw=1/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,40 +1,15 @@
|
||||
<template>
|
||||
<div :class="grouped ? 'el-button-group' : ''">
|
||||
<el-button v-for="item in iActions" :key="item.name" :size="size" v-bind="item" @click="handleClick(item.name)">
|
||||
<el-tooltip v-if="['actionExport', 'actionImport', 'actionRefresh'].indexOf(item.name) !== -1" effect="dark" :content="item.tip" placement="top">
|
||||
<i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }}
|
||||
</el-tooltip>
|
||||
<span v-else>
|
||||
<i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }}
|
||||
</span>
|
||||
</el-button>
|
||||
<el-dropdown v-if="iMoreActions.length > 0" trigger="click" :placement="moreActionsPlacement" @command="handleClick">
|
||||
<el-button :size="size" :type="moreActionsType" class="btn-more-actions">
|
||||
{{ iMoreActionsTitle }}<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="item in iMoreActions" :key="item.name" :command="item.name" v-bind="item" @click="handleClick(item.name)">{{ item.title }} </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<DataActions :actions="iActions" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataActions from '@/components/DataActions'
|
||||
export default {
|
||||
name: 'ActionsGroup',
|
||||
components: {
|
||||
DataActions
|
||||
},
|
||||
props: {
|
||||
grouped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -61,74 +36,22 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
iActions() {
|
||||
return this.cleanActions(this.actions)
|
||||
},
|
||||
iMoreActions() {
|
||||
return this.cleanActions(this.moreActions)
|
||||
},
|
||||
totalActions() {
|
||||
return [...this.actions, ...this.moreActions]
|
||||
},
|
||||
totalNamedActions() {
|
||||
const actions = {}
|
||||
for (const action of this.totalActions) {
|
||||
if (!action || !action.hasOwnProperty('name')) {
|
||||
continue
|
||||
}
|
||||
actions[action.name] = action
|
||||
const actions = [...this.actions]
|
||||
if (this.iMoreAction && this.iMoreAction.dropdown.length > 0) {
|
||||
actions.push(this.iMoreAction)
|
||||
}
|
||||
return actions
|
||||
},
|
||||
iMoreAction() {
|
||||
return {
|
||||
name: 'moreActions',
|
||||
title: this.iMoreActionsTitle,
|
||||
dropdown: this.moreActions || []
|
||||
}
|
||||
},
|
||||
iMoreActionsTitle() {
|
||||
return this.moreActionsTitle || this.$t('common.MoreActions')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(item) {
|
||||
const action = this.totalNamedActions[item]
|
||||
if (action && action.callback) {
|
||||
action.callback(action)
|
||||
} else {
|
||||
this.$log.debug('No callback found')
|
||||
}
|
||||
this.$emit('actionClick', item)
|
||||
},
|
||||
checkItem(item, attr, defaults) {
|
||||
if (!item) {
|
||||
return true
|
||||
}
|
||||
let ok = item[attr]
|
||||
if (ok && typeof ok === 'function') {
|
||||
ok = ok(item)
|
||||
} else if (ok == null) {
|
||||
ok = defaults === undefined ? true : defaults
|
||||
}
|
||||
return ok
|
||||
},
|
||||
cleanActions(actions) {
|
||||
const cleanedActions = []
|
||||
const cloneActions = _.cloneDeep(actions)
|
||||
for (const v of cloneActions) {
|
||||
if (!v) {
|
||||
continue
|
||||
}
|
||||
const action = Object.assign({}, v)
|
||||
// 是否拥有这个action
|
||||
const has = this.checkItem(action, 'has')
|
||||
delete action['has']
|
||||
if (!has) {
|
||||
continue
|
||||
}
|
||||
// 是否是disabled
|
||||
const can = this.checkItem(action, 'can')
|
||||
delete action['can']
|
||||
action.disabled = !can
|
||||
cleanedActions.push(action)
|
||||
// 删掉callback,避免前台看到
|
||||
delete action['callback']
|
||||
}
|
||||
return cleanedActions
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<AssetSelect ref="assetSelect" :can-select="canSelect" />
|
||||
<AssetSelect ref="assetSelect" :disabled="disabled" :can-select="canSelect" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<el-button :type="type" size="small" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
<el-button :type="type" size="small" :disabled="disabled" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -38,6 +38,10 @@ export default {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: [Array, Number, String],
|
||||
default: () => []
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
<script>
|
||||
import TreeTable from '@/components/TreeTable'
|
||||
import { DetailFormatter } from '@/components/ListTable/formatters'
|
||||
import Select2 from '@/components/Select2'
|
||||
import { DetailFormatter } from '@/components/TableFormatters'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import Dialog from '@/components/Dialog'
|
||||
|
||||
export default {
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,58 +1,75 @@
|
||||
<template><div>
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
|
||||
<div v-if="MFAConfirmed">
|
||||
<el-form label-position="right" label-width="80px" :model="MFAInfo">
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="iTableConfig" :header-actions="headerActions" />
|
||||
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
|
||||
<div v-if="MFAConfirmed">
|
||||
<el-form label-position="right" label-width="80px" :model="MFAInfo">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="MFAInfo.hostname" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="MFAInfo.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="MFAInfo.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-row v-else :gutter="20">
|
||||
<el-col :span="4">
|
||||
<div style="line-height: 34px;text-align: center">MFA</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="MFAInput" />
|
||||
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</Dialog>
|
||||
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
|
||||
<el-form label-position="right" label-width="80px" :model="dialogInfo">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="MFAInfo.hostname" disabled />
|
||||
<el-input v-model="dialogInfo.hostname" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="MFAInfo.username" disabled />
|
||||
<el-input v-model="dialogInfo.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="MFAInfo.password" type="password" show-password />
|
||||
<el-input v-model="dialogInfo.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.sshkey')">
|
||||
<input type="file" @change="Onchange">
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-row v-else :gutter="20">
|
||||
<el-col :span="4">
|
||||
<div style="line-height: 34px;text-align: center">MFA</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="MFAInput" />
|
||||
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</Dialog>
|
||||
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
|
||||
<el-form label-position="right" label-width="80px" :model="dialogInfo">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="dialogInfo.hostname" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="dialogInfo.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="dialogInfo.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.sshkey')">
|
||||
<input type="file" @change="Onchange">
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
<Dialog :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
|
||||
<el-form label-position="left" style="padding-left: 50px">
|
||||
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
|
||||
<el-radio-group v-model="exportTypeOption">
|
||||
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
|
||||
<el-radio-group v-model="exportOption">
|
||||
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import Dialog from '@/components/Dialog'
|
||||
import { ActionsFormatter, DateFormatter } from '@/components/ListTable/formatters'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
|
||||
import { ActionsFormatter, DateFormatter } from '@/components/TableFormatters'
|
||||
|
||||
export default {
|
||||
name: 'Detail',
|
||||
@@ -65,6 +82,26 @@ export default {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
searchExclude: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
extraQuery: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
canExportAll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canExportSelected: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canExportFiltered: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -88,6 +125,14 @@ export default {
|
||||
hasExport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
tableConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -107,38 +152,38 @@ export default {
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
private_key: ''
|
||||
},
|
||||
tableConfig: {
|
||||
selectedRows: '',
|
||||
dialogStatus: '',
|
||||
showExportDialog: false,
|
||||
exportOption: 'all',
|
||||
exportTypeOption: 'csv',
|
||||
defaultTableConfig: {
|
||||
url: this.url,
|
||||
columns: [
|
||||
{
|
||||
prop: 'hostname',
|
||||
columns: ['hostname', 'ip', 'username', 'version', 'date_created', 'actions'],
|
||||
columnsMeta: {
|
||||
'hostname': {
|
||||
label: this.$t('assets.Hostname'),
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'ip',
|
||||
'ip': {
|
||||
label: this.$t('assets.ip'),
|
||||
width: '120px'
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
'username': {
|
||||
label: this.$t('assets.Username'),
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'version',
|
||||
'version': {
|
||||
label: this.$t('assets.Version'),
|
||||
width: '70px'
|
||||
},
|
||||
{
|
||||
prop: 'date_created',
|
||||
'date_created': {
|
||||
label: this.$t('assets.date_joined'),
|
||||
formatter: DateFormatter
|
||||
},
|
||||
{
|
||||
prop: 'id',
|
||||
'actions': {
|
||||
label: this.$t('common.Action'),
|
||||
align: 'center',
|
||||
width: 150,
|
||||
@@ -146,6 +191,7 @@ export default {
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
@@ -153,7 +199,8 @@ export default {
|
||||
title: this.$t('common.View'),
|
||||
type: 'primary',
|
||||
callback: function(val) {
|
||||
this.MFAInfo.asset = val.cellValue
|
||||
this.dialogStatus = 'viewAutoInfo'
|
||||
this.MFAInfo.asset = val.row.id
|
||||
if (!this.needMFAVerify) {
|
||||
this.showMFADialog = true
|
||||
this.MFAConfirmed = true
|
||||
@@ -173,7 +220,7 @@ export default {
|
||||
title: this.$t('common.Delete'),
|
||||
type: 'primary',
|
||||
callback: (val) => {
|
||||
this.$axios.delete(`/api/v1/assets/asset-users/${val.cellValue}/`).then(() => {
|
||||
this.$axios.delete(`/api/v1/assets/asset-users/${val.row.id}/`).then(() => {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
})
|
||||
@@ -184,7 +231,7 @@ export default {
|
||||
title: this.$t('common.Test'),
|
||||
callback: (val) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/asset-users/tasks/?id=${val.cellValue}`,
|
||||
`/api/v1/assets/asset-users/tasks/?id=${val.row.id}`,
|
||||
{ action: 'test' }
|
||||
).then(res => {
|
||||
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
|
||||
@@ -194,6 +241,7 @@ export default {
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: !this.$store.getters.currentOrgIsRoot,
|
||||
callback: function(val) {
|
||||
this.showDialog = true
|
||||
this.dialogInfo.asset = val.row.asset
|
||||
@@ -204,18 +252,17 @@ export default {
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
extraQuery: {
|
||||
latest: 1
|
||||
}
|
||||
},
|
||||
extraQuery: this.extraQuery || { latest: 1 }
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasBulkDelete: false,
|
||||
hasMoreActions: false,
|
||||
hasImport: this.hasImport,
|
||||
hasExport: this.hasExport,
|
||||
hasSearch: true,
|
||||
searchConfig: {
|
||||
exclude: this.searchExclude,
|
||||
options: [
|
||||
{
|
||||
label: this.$t('assets.OnlyLatestVersion'),
|
||||
@@ -249,25 +296,62 @@ export default {
|
||||
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
|
||||
const now = new Date()
|
||||
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
|
||||
},
|
||||
iTableConfig() {
|
||||
const columnsMeta = Object.assign({}, this.defaultTableConfig.columnsMeta, this.tableConfig.columnsMeta || {})
|
||||
const config = Object.assign(this.defaultTableConfig, this.tableConfig)
|
||||
config.columnsMeta = columnsMeta
|
||||
return config
|
||||
},
|
||||
exportOptions() {
|
||||
return [
|
||||
{
|
||||
label: this.$t('common.imExport.ExportAll'),
|
||||
value: 'all',
|
||||
can: this.canExportAll
|
||||
},
|
||||
{
|
||||
label: this.$t('common.imExport.ExportOnlySelectedItems'),
|
||||
value: 'selected',
|
||||
can: this.selectedRows.length > 0 && this.canExportSelected
|
||||
},
|
||||
{
|
||||
label: this.$t('common.imExport.ExportOnlyFiltered'),
|
||||
value: 'filtered',
|
||||
can: this.tableHasQuery() && this.canExportFiltered
|
||||
}
|
||||
]
|
||||
},
|
||||
exportTypeOptions() {
|
||||
return [
|
||||
{
|
||||
label: 'CSV',
|
||||
value: 'csv',
|
||||
can: true
|
||||
},
|
||||
{
|
||||
label: 'Excel',
|
||||
value: 'xlsx',
|
||||
can: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
this.$set(this.iTableConfig, 'url', iNew)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
const actionColumn = this.iTableConfig.columns[this.iTableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.handleExport) {
|
||||
this.headerActions.handleExport = this.handleExport
|
||||
}
|
||||
this.headerActions.handleExport = this.handleExport || this.defaultHandleExport
|
||||
if (this.handleImport) {
|
||||
this.headerActions.handleImport = this.handleImport
|
||||
}
|
||||
@@ -284,12 +368,17 @@ export default {
|
||||
).then(
|
||||
res => {
|
||||
this.$store.dispatch('users/setMFAVerify')
|
||||
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
|
||||
this.MFAConfirmed = true
|
||||
this.MFAInfo.hostname = res.hostname
|
||||
this.MFAInfo.password = res.password
|
||||
this.MFAInfo.username = res.username
|
||||
})
|
||||
if (this.dialogStatus === 'export') {
|
||||
this.showMFADialog = false
|
||||
this.showExportDialog = true
|
||||
} else {
|
||||
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
|
||||
this.MFAConfirmed = true
|
||||
this.MFAInfo.hostname = res.hostname
|
||||
this.MFAInfo.password = res.password
|
||||
this.MFAInfo.username = res.username
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -310,7 +399,7 @@ export default {
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
private_key: ''
|
||||
}
|
||||
this.showDialog = false
|
||||
this.$refs.ListTable.reloadTable()
|
||||
@@ -320,7 +409,7 @@ export default {
|
||||
// TODO 校验文件类型
|
||||
const reader = new FileReader()
|
||||
reader.onload = function() {
|
||||
vm.dialogInfo.key = this.result
|
||||
vm.dialogInfo.private_key = this.result
|
||||
}
|
||||
reader.readAsText(
|
||||
e.target.files[0]
|
||||
@@ -334,8 +423,8 @@ export default {
|
||||
if (this.dialogInfo.password !== '') {
|
||||
data.password = this.dialogInfo.password
|
||||
}
|
||||
if (this.dialogInfo.key !== '') {
|
||||
data.key = this.dialogInfo.key
|
||||
if (this.dialogInfo.private_key !== '') {
|
||||
data.private_key = this.dialogInfo.private_key
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/asset-users/`,
|
||||
@@ -350,15 +439,91 @@ export default {
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
private_key: ''
|
||||
}
|
||||
this.showDialog = false
|
||||
this.$refs.ListTable.reloadTable()
|
||||
},
|
||||
tableQuery() {
|
||||
const listTableRef = this.$refs.ListTable
|
||||
if (!listTableRef) {
|
||||
return {}
|
||||
}
|
||||
const query = listTableRef.dataTable.getQuery()
|
||||
delete query['limit']
|
||||
delete query['offset']
|
||||
delete query['date_from']
|
||||
delete query['date_to']
|
||||
return query
|
||||
},
|
||||
tableHasQuery() {
|
||||
return Object.keys(this.tableQuery()).length > 0
|
||||
},
|
||||
defaultHandleExport({ selectedRows }) {
|
||||
this.selectedRows = selectedRows
|
||||
this.dialogStatus = 'export'
|
||||
if (!this.needMFAVerify) {
|
||||
this.showMFADialog = false
|
||||
this.showExportDialog = true
|
||||
} else {
|
||||
this.showMFADialog = true
|
||||
}
|
||||
},
|
||||
downloadCsv(url) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
async performExport(selectRows, exportOption, q) {
|
||||
const url = `/api/v1/assets/asset-user-auth-infos/`
|
||||
const query = Object.assign({}, q)
|
||||
if (exportOption === 'selected') {
|
||||
const resources = []
|
||||
const data = selectRows
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
resources.push(data[index].id)
|
||||
}
|
||||
const spm = await createSourceIdCache(resources)
|
||||
query['spm'] = spm.spm
|
||||
} else if (exportOption === 'filtered') {
|
||||
// console.log(listTableRef)
|
||||
// console.log(listTableRef.dataTable)
|
||||
// delete query['limit']
|
||||
// delete query['offset']
|
||||
}
|
||||
query['format'] = this.exportTypeOption
|
||||
const queryStr =
|
||||
(url.indexOf('?') > -1 ? '&' : '?') +
|
||||
queryUtil.stringify(query, '=', '&')
|
||||
return this.downloadCsv(url + queryStr)
|
||||
},
|
||||
async performExportConfirm() {
|
||||
const listTableRef = this.$refs.ListTable
|
||||
const query = listTableRef.dataTable.getQuery()
|
||||
delete query['limit']
|
||||
delete query['offset']
|
||||
return this.performExport(this.selectedRows, this.exportOption, query)
|
||||
},
|
||||
async handleExportConfirm() {
|
||||
await this.performExportConfirm()
|
||||
this.showExportDialog = false
|
||||
},
|
||||
handleExportCancel() {
|
||||
this.showExportDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
<style lang='scss' scoped>
|
||||
.export-item {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.export-form >>> .el-form-item__label {
|
||||
line-height: 2
|
||||
}
|
||||
</style>
|
||||
|
||||
81
src/components/AutoDataForm/components/NestedField.vue
Normal file
81
src/components/AutoDataForm/components/NestedField.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<DataForm
|
||||
:fields="iFields"
|
||||
:form="value"
|
||||
style="margin-left: -26%;margin-right: -6%"
|
||||
v-bind="kwargs"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DataForm } from '@/components'
|
||||
|
||||
export default {
|
||||
name: 'NestedField',
|
||||
components: {
|
||||
DataForm
|
||||
},
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
errors: {
|
||||
type: [Object, String],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
kwargs: {
|
||||
hasReset: false,
|
||||
hasSaveContinue: false,
|
||||
defaultButton: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iFields() {
|
||||
const fields = this.fields
|
||||
if (this.errors && typeof this.errors === 'object') {
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let [name, error] of Object.entries(this.errors)) {
|
||||
const field = fields.find((v) => v.prop === name)
|
||||
if (!field) {
|
||||
continue
|
||||
}
|
||||
this.$log.debug(`${name}: ${error}`)
|
||||
if (typeof error === 'object' && !Array.isArray(error)) {
|
||||
error = this.objectToString(error)
|
||||
}
|
||||
field.attrs.error = error.toString()
|
||||
}
|
||||
}
|
||||
this.$log.debug('Fields change: ', fields, this.errors)
|
||||
return fields
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
objectToString(obj) {
|
||||
let data = ''
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'object') {
|
||||
value = this.objectToString(value)
|
||||
}
|
||||
data += ` ${key}: ${value} `
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,15 +1,13 @@
|
||||
<template>
|
||||
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" v-bind="$attrs" v-on="$listeners">
|
||||
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i != 0" />
|
||||
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" :form="iForm" v-bind="$attrs" v-on="$listeners">
|
||||
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i !== 0" />
|
||||
</DataForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '../DataForm'
|
||||
import FormGroupHeader from '@/components/FormGroupHeader'
|
||||
// import { optionUrlMeta } from '@/api/common'
|
||||
import rules from '@/components/DataForm/rules'
|
||||
import Select2 from '@/components/Select2'
|
||||
import { FormFieldGenerator } from '@/components/AutoDataForm/utils'
|
||||
export default {
|
||||
name: 'AutoDataForm',
|
||||
components: {
|
||||
@@ -31,6 +29,10 @@ export default {
|
||||
return []
|
||||
}
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
fieldsMeta: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
@@ -38,136 +40,51 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: {},
|
||||
remoteMeta: {},
|
||||
totalFields: [],
|
||||
loading: true,
|
||||
groups: []
|
||||
groups: [],
|
||||
iForm: this.form
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.optionUrlMeta()
|
||||
this.optionUrlMetaAndGenerateColumns()
|
||||
},
|
||||
methods: {
|
||||
optionUrlMeta() {
|
||||
optionUrlMetaAndGenerateColumns() {
|
||||
this.$store.dispatch('common/getUrlMeta', { url: this.url }).then(data => {
|
||||
this.meta = data.actions[this.method.toUpperCase()] || {}
|
||||
this.remoteMeta = data.actions[this.method.toUpperCase()] || {}
|
||||
this.generateColumns()
|
||||
this.cleanFormValue()
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
this.$log.error(err)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
generateFieldByType(type, field, fieldMeta) {
|
||||
switch (type) {
|
||||
case 'choice':
|
||||
type = 'radio-group'
|
||||
field.options = fieldMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
break
|
||||
case 'datetime':
|
||||
type = 'date-picker'
|
||||
field.el = {
|
||||
type: 'datetime'
|
||||
}
|
||||
break
|
||||
case 'field':
|
||||
type = ''
|
||||
field.component = Select2
|
||||
if (fieldMeta.required) {
|
||||
field.el.clearable = false
|
||||
}
|
||||
break
|
||||
case 'string':
|
||||
type = 'input'
|
||||
if (!fieldMeta.max_length) {
|
||||
field.el.type = 'textarea'
|
||||
field.el.rows = 3
|
||||
}
|
||||
break
|
||||
default:
|
||||
type = 'input'
|
||||
break
|
||||
}
|
||||
if (type === 'radio-group') {
|
||||
const options = fieldMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
if (options.length > 4) {
|
||||
type = 'select'
|
||||
field.el.filterable = true
|
||||
}
|
||||
}
|
||||
field.type = type
|
||||
return field
|
||||
},
|
||||
generateFieldByName(name, field) {
|
||||
switch (name) {
|
||||
case 'email':
|
||||
field.el.type = 'email'
|
||||
break
|
||||
case 'password':
|
||||
field.el.type = 'password'
|
||||
break
|
||||
case 'comment':
|
||||
field.el.type = 'textarea'
|
||||
break
|
||||
}
|
||||
return field
|
||||
},
|
||||
generateFieldByOther(field, fieldMeta) {
|
||||
const filedRules = field.rules || []
|
||||
if (fieldMeta.required) {
|
||||
if (field.type === 'input') {
|
||||
filedRules.push(rules.Required)
|
||||
} else {
|
||||
filedRules.push(rules.RequiredChange)
|
||||
}
|
||||
}
|
||||
field.rules = filedRules
|
||||
return field
|
||||
},
|
||||
generateField(name) {
|
||||
let field = { id: name, prop: name, el: {}, attrs: {}}
|
||||
const fieldMeta = this.meta[name] || {}
|
||||
field.label = fieldMeta.label
|
||||
field = this.generateFieldByType(fieldMeta.type, field, fieldMeta)
|
||||
field = this.generateFieldByName(name, field)
|
||||
field = this.generateFieldByOther(field, fieldMeta)
|
||||
field = Object.assign(field, this.fieldsMeta[name] || {})
|
||||
_.set(field, 'attrs.error', '')
|
||||
return field
|
||||
},
|
||||
generateFieldGroup(data) {
|
||||
const [groupTitle, fields] = data
|
||||
this.groups.push({
|
||||
id: groupTitle,
|
||||
title: groupTitle,
|
||||
name: fields[0]
|
||||
})
|
||||
return this.generateFields(fields)
|
||||
},
|
||||
generateFields(data) {
|
||||
let fields = []
|
||||
for (let field of data) {
|
||||
if (field instanceof Array) {
|
||||
const items = this.generateFieldGroup(field)
|
||||
fields = [...fields, ...items]
|
||||
} else if (typeof field === 'string') {
|
||||
field = this.generateField(field)
|
||||
fields.push(field)
|
||||
} else if (field instanceof Object) {
|
||||
this.errors[field.prop] = ''
|
||||
_.set(field, 'attrs.error', '')
|
||||
fields.push(field)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
},
|
||||
generateColumns() {
|
||||
this.totalFields = this.generateFields(this.fields)
|
||||
const generator = new FormFieldGenerator()
|
||||
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
|
||||
this.groups = generator.groups
|
||||
this.$log.debug('Total fields: ', this.totalFields)
|
||||
},
|
||||
_cleanFormValue(form, remoteMeta) {
|
||||
for (const [k, v] of Object.entries(remoteMeta)) {
|
||||
if (v.default === undefined) {
|
||||
continue
|
||||
}
|
||||
const valueSet = form[k]
|
||||
if (valueSet !== undefined) {
|
||||
continue
|
||||
}
|
||||
if (v.type === 'nested object' && typeof valueSet === 'object') {
|
||||
this._cleanFormValue(valueSet, v.children)
|
||||
}
|
||||
form[k] = v.default
|
||||
}
|
||||
},
|
||||
cleanFormValue() {
|
||||
this._cleanFormValue(this.iForm, this.remoteMeta)
|
||||
},
|
||||
setFieldError(name, error) {
|
||||
const field = this.totalFields.find((v) => v.prop === name)
|
||||
@@ -177,7 +94,11 @@ export default {
|
||||
if (field.attrs.error === error) {
|
||||
error += '.'
|
||||
}
|
||||
field.attrs.error = error
|
||||
if (field.type === 'nestedField') {
|
||||
field.el.errors = error
|
||||
} else {
|
||||
field.attrs.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
src/components/AutoDataForm/utils.js
Normal file
155
src/components/AutoDataForm/utils.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import Vue from 'vue'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import NestedField from '@/components/AutoDataForm/components/NestedField'
|
||||
import rules from '@/components/DataForm/rules'
|
||||
import { assignIfNot } from '@/utils/common'
|
||||
|
||||
export class FormFieldGenerator {
|
||||
constructor() {
|
||||
this.groups = []
|
||||
}
|
||||
generateFieldByType(type, field, fieldMeta, fieldRemoteMeta) {
|
||||
switch (type) {
|
||||
case 'choice':
|
||||
type = 'radio-group'
|
||||
if (!fieldRemoteMeta.read_only) {
|
||||
field.options = fieldRemoteMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'datetime':
|
||||
type = 'date-picker'
|
||||
field.el = {
|
||||
type: 'datetime'
|
||||
}
|
||||
break
|
||||
case 'field':
|
||||
type = ''
|
||||
field.component = Select2
|
||||
if (fieldRemoteMeta.required) {
|
||||
field.el.clearable = false
|
||||
}
|
||||
break
|
||||
case 'string':
|
||||
type = 'input'
|
||||
if (!fieldRemoteMeta.max_length) {
|
||||
field.el.type = 'textarea'
|
||||
field.el.rows = 3
|
||||
}
|
||||
if (fieldRemoteMeta.write_only) {
|
||||
field.el.type = 'password'
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
type = 'checkbox'
|
||||
break
|
||||
case 'nested object':
|
||||
type = 'nestedField'
|
||||
field.component = NestedField
|
||||
field.label = ''
|
||||
field.labelWidth = 0
|
||||
field.el.fields = this.generateNestFields(field, fieldMeta, fieldRemoteMeta)
|
||||
field.el.errors = {}
|
||||
Vue.$log.debug('All fields in generate: ', field.el.allFields)
|
||||
break
|
||||
default:
|
||||
type = 'input'
|
||||
break
|
||||
}
|
||||
if (type === 'radio-group') {
|
||||
if (!fieldRemoteMeta.read_only) {
|
||||
const options = fieldRemoteMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
if (options.length > 4) {
|
||||
type = 'select'
|
||||
field.el.filterable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
field.type = type
|
||||
return field
|
||||
}
|
||||
generateNestFields(field, fieldMeta, fieldRemoteMeta) {
|
||||
const fields = []
|
||||
const nestedFields = fieldMeta.fields || []
|
||||
const nestedFieldsMeta = fieldMeta.fieldsMeta || {}
|
||||
const nestedFieldsRemoteMeta = fieldRemoteMeta.children || {}
|
||||
for (const name of nestedFields) {
|
||||
const f = this.generateField(name, nestedFieldsMeta, nestedFieldsRemoteMeta)
|
||||
fields.push(f)
|
||||
}
|
||||
Vue.$log.debug('NestFields: ', fields)
|
||||
return fields
|
||||
}
|
||||
generateFieldByName(name, field) {
|
||||
switch (name) {
|
||||
case 'email':
|
||||
field.el.type = 'email'
|
||||
break
|
||||
case 'password':
|
||||
field.el.type = 'password'
|
||||
break
|
||||
case 'comment':
|
||||
field.el.type = 'textarea'
|
||||
break
|
||||
}
|
||||
return field
|
||||
}
|
||||
generateFieldByOther(field, fieldMeta, fieldRemoteMeta) {
|
||||
const filedRules = field.rules || []
|
||||
if (fieldRemoteMeta.required) {
|
||||
if (field.type === 'input') {
|
||||
filedRules.push(rules.Required)
|
||||
} else {
|
||||
filedRules.push(rules.RequiredChange)
|
||||
}
|
||||
}
|
||||
field.rules = filedRules
|
||||
return field
|
||||
}
|
||||
generateField(name, fieldsMeta, remoteFieldsMeta) {
|
||||
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
|
||||
const remoteFieldMeta = remoteFieldsMeta[name] || {}
|
||||
const fieldMeta = fieldsMeta[name] || {}
|
||||
field.label = remoteFieldMeta.label
|
||||
field.helpText = remoteFieldMeta.help_text
|
||||
field = this.generateFieldByType(remoteFieldMeta.type, field, fieldMeta, remoteFieldMeta)
|
||||
field = this.generateFieldByName(name, field)
|
||||
field = this.generateFieldByOther(field, fieldMeta, remoteFieldMeta)
|
||||
const el = assignIfNot(fieldMeta.el || {}, field.el)
|
||||
const rules = fieldMeta.rules || field.rules
|
||||
field = Object.assign(field, fieldMeta)
|
||||
field.el = el
|
||||
field.rules = rules
|
||||
_.set(field, 'attrs.error', '')
|
||||
// Vue.$log.debug('Generate field: ', name, field)
|
||||
return field
|
||||
}
|
||||
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {
|
||||
const [groupTitle, fields] = field
|
||||
this.groups.push({
|
||||
id: groupTitle,
|
||||
title: groupTitle,
|
||||
name: fields[0]
|
||||
})
|
||||
return this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
|
||||
}
|
||||
generateFields(_fields, fieldsMeta, remoteFieldsMeta) {
|
||||
let fields = []
|
||||
for (let field of _fields) {
|
||||
if (field instanceof Array) {
|
||||
const items = this.generateFieldGroup(field, fieldsMeta, remoteFieldsMeta)
|
||||
fields = [...fields, ...items]
|
||||
} else if (typeof field === 'string') {
|
||||
field = this.generateField(field, fieldsMeta, remoteFieldsMeta)
|
||||
fields.push(field)
|
||||
} else if (field instanceof Object) {
|
||||
this.errors[field.prop] = ''
|
||||
fields.push(field)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TagSearch :options="options" v-bind="$attrs" v-on="$listeners" />
|
||||
<TagSearch :options="iOption" v-bind="$attrs" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -23,9 +23,22 @@ export default {
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iOption() {
|
||||
return this.options.concat(this.internalOptions)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
options() {
|
||||
// 空函数,方便子组件刷新
|
||||
},
|
||||
url() {
|
||||
this.genericOptions()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -36,6 +49,7 @@ export default {
|
||||
methods: {
|
||||
async genericOptions() {
|
||||
const vm = this // 透传This
|
||||
vm.internalOptions = [] // 重置
|
||||
const data = await this.optionUrlMeta()
|
||||
const meta = data.actions['GET'] || {}
|
||||
for (const [name, field] of Object.entries(meta)) {
|
||||
@@ -49,7 +63,6 @@ export default {
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
value: name
|
||||
|
||||
}
|
||||
if (field.type === 'choice' && field.choices) {
|
||||
option.children = field.choices.map(item => {
|
||||
@@ -69,7 +82,7 @@ export default {
|
||||
{ label: this.$t('common.No'), value: false }
|
||||
]
|
||||
}
|
||||
vm.options.push(option)
|
||||
vm.internalOptions.push(option)
|
||||
}
|
||||
},
|
||||
optionUrlMeta() {
|
||||
@@ -81,5 +94,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="showColumnSettingPopover"
|
||||
:title="$t('common.CustomCol')"
|
||||
:visible.sync="showColumnSettingPopover"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
width="35%"
|
||||
top="10%"
|
||||
@confirm="handleColumnConfirm()"
|
||||
>
|
||||
<el-alert type="success">
|
||||
{{ this.$t('common.TableColSettingInfo') }}
|
||||
</el-alert>
|
||||
<el-checkbox-group
|
||||
v-model="iCurrentColumns"
|
||||
>
|
||||
<el-row>
|
||||
<el-col
|
||||
v-for="item in totalColumnsList"
|
||||
:key="item.prop"
|
||||
:span="8"
|
||||
style="margin-top:5px;"
|
||||
>
|
||||
<el-checkbox
|
||||
:label="item.prop"
|
||||
:disabled="
|
||||
item.prop==='id' ||
|
||||
item.prop==='actions' ||
|
||||
minColumns.indexOf(item.prop)!==-1
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-checkbox-group>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index'
|
||||
export default {
|
||||
name: 'ColumnSettingPopover',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
totalColumnsList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentColumns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
minColumns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showColumnSettingPopover: false,
|
||||
iCurrentColumns: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('showColumnSettingPopover', ({ url }) => {
|
||||
if (url === this.url) {
|
||||
this.showColumnSettingPopover = true
|
||||
this.iCurrentColumns = this.currentColumns
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleColumnConfirm() {
|
||||
this.showColumnSettingPopover = false
|
||||
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
</style>
|
||||
@@ -1,20 +1,36 @@
|
||||
<template>
|
||||
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" />
|
||||
<div>
|
||||
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" @filter-change="filterChange" />
|
||||
<ColumnSettingPopover
|
||||
:current-columns="popoverColumns.currentCols"
|
||||
:total-columns-list="popoverColumns.totalColumnsList"
|
||||
:min-columns="popoverColumns.minCols"
|
||||
:url="config.url"
|
||||
@columnsUpdate="handlePopoverColumnsChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script type="text/jsx">
|
||||
import DataTable from '../DataTable'
|
||||
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters'
|
||||
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/TableFormatters'
|
||||
import i18n from '@/i18n/i18n'
|
||||
import ColumnSettingPopover from './components/ColumnSettingPopover'
|
||||
import { newURL } from '@/utils/common'
|
||||
export default {
|
||||
name: 'AutoDataTable',
|
||||
components: {
|
||||
DataTable
|
||||
DataTable,
|
||||
ColumnSettingPopover
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
filterTable: {
|
||||
type: Function,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -23,8 +39,18 @@ export default {
|
||||
method: 'get',
|
||||
autoConfig: {},
|
||||
iConfig: {},
|
||||
meta: {}
|
||||
meta: {},
|
||||
cleanedColumnsShow: {},
|
||||
totalColumns: [],
|
||||
popoverColumns: {
|
||||
totalColumnsList: [],
|
||||
minCols: [],
|
||||
currentCols: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
@@ -40,10 +66,18 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async optionUrlMetaAndGenCols() {
|
||||
if (this.config.url === '') { return }
|
||||
const url = (this.config.url.indexOf('?') === -1) ? `${this.config.url}?draw=1&display=1` : `${this.config.url}&draw=1&display=1`
|
||||
this.$store.dispatch('common/getUrlMeta', { url: url }).then(data => {
|
||||
this.meta = data.actions[this.method.toUpperCase()] || {}
|
||||
this.generateColumns()
|
||||
const method = this.method.toUpperCase()
|
||||
this.meta = data.actions && data.actions[method] ? data.actions[method] : {}
|
||||
this.generateTotalColumns()
|
||||
}).then(() => {
|
||||
// 根据当前列重新生成最终渲染表格
|
||||
this.filterShowColumns()
|
||||
}).then(() => {
|
||||
// 生成给子组件使用的TotalColList
|
||||
this.generatePopoverColumns()
|
||||
}).catch((error) => {
|
||||
this.$log.error('Error occur: ', error)
|
||||
}).finally(() => {
|
||||
@@ -59,7 +93,7 @@ export default {
|
||||
break
|
||||
case 'actions':
|
||||
col = {
|
||||
prop: 'id',
|
||||
prop: 'actions',
|
||||
label: i18n.t('common.Actions'),
|
||||
align: 'center',
|
||||
width: '150px',
|
||||
@@ -118,6 +152,37 @@ export default {
|
||||
}
|
||||
return col
|
||||
},
|
||||
addFilterIfNeed(col) {
|
||||
if (col.prop) {
|
||||
const column = this.meta[col.prop] || {}
|
||||
if (!column.filter) {
|
||||
return col
|
||||
}
|
||||
if (column.type === 'boolean') {
|
||||
col.filters = [
|
||||
{ text: this.$t('common.Yes'), value: true },
|
||||
{ text: this.$t('common.No'), value: false }
|
||||
]
|
||||
col.sortable = false
|
||||
col['column-key'] = col.prop
|
||||
}
|
||||
if (column.type === 'choice' && column.choices) {
|
||||
col.filters = column.choices.map(item => {
|
||||
if (typeof (item.value) === 'boolean') {
|
||||
if (item.value) {
|
||||
return { text: item.display_name, value: 'True' }
|
||||
} else {
|
||||
return { text: item.display_name, value: 'False' }
|
||||
}
|
||||
}
|
||||
return { text: item.display_name, value: item.value }
|
||||
})
|
||||
col.sortable = false
|
||||
col['column-key'] = col.prop
|
||||
}
|
||||
}
|
||||
return col
|
||||
},
|
||||
generateColumn(name) {
|
||||
const colMeta = this.meta[name] || {}
|
||||
const customMeta = this.config.columnsMeta ? this.config.columnsMeta[name] : {}
|
||||
@@ -127,9 +192,10 @@ export default {
|
||||
col = this.generateColumnByType(colMeta.type, col)
|
||||
col = Object.assign(col, customMeta)
|
||||
col = this.addHelpTipsIfNeed(col)
|
||||
col = this.addFilterIfNeed(col)
|
||||
return col
|
||||
},
|
||||
generateColumns() {
|
||||
generateTotalColumns() {
|
||||
const config = _.cloneDeep(this.config)
|
||||
const columns = []
|
||||
for (let col of config.columns) {
|
||||
@@ -140,8 +206,85 @@ export default {
|
||||
columns.push(col)
|
||||
}
|
||||
}
|
||||
// 第一次初始化时记录 totalColumns
|
||||
this.totalColumns = columns
|
||||
config.columns = columns
|
||||
this.iConfig = config
|
||||
},
|
||||
// 生成给子组件使用的TotalColList
|
||||
cleanColumnsShow() {
|
||||
const totalColumnsNames = this.totalColumns.map(obj => obj.prop)
|
||||
// 默认列
|
||||
let defaultColumnsNames = _.get(this.iConfig, 'columnsShow.default', [])
|
||||
if (defaultColumnsNames.length === 0) {
|
||||
defaultColumnsNames = totalColumnsNames
|
||||
}
|
||||
// Clean it
|
||||
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
|
||||
|
||||
// 最小列
|
||||
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['action', 'id'])
|
||||
.filter(n => defaultColumnsNames.indexOf(n) > -1)
|
||||
|
||||
// 应该显示的列
|
||||
const _tableConfig = localStorage.getItem('tableConfig')
|
||||
? JSON.parse(localStorage.getItem('tableConfig'))
|
||||
: {}
|
||||
const tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
|
||||
const configShowColumnsNames = _.get(_tableConfig[tableName], 'showColumns', null)
|
||||
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
|
||||
if (showColumnsNames.length === 0) {
|
||||
showColumnsNames = totalColumnsNames
|
||||
}
|
||||
// 校对显示的列,是不是包含最小列
|
||||
minColumnsNames.forEach((v, i) => {
|
||||
if (showColumnsNames.indexOf(v) === -1) {
|
||||
showColumnsNames.push(v)
|
||||
}
|
||||
})
|
||||
// Clean it
|
||||
showColumnsNames = totalColumnsNames.filter(n => showColumnsNames.indexOf(n) > -1)
|
||||
|
||||
this.cleanedColumnsShow = {
|
||||
default: defaultColumnsNames,
|
||||
show: showColumnsNames,
|
||||
min: minColumnsNames,
|
||||
configShow: configShowColumnsNames
|
||||
}
|
||||
this.$log.debug('Cleaned columns show: ', this.cleanedColumnsShow)
|
||||
},
|
||||
filterShowColumns() {
|
||||
this.cleanColumnsShow()
|
||||
this.iConfig.columns = this.totalColumns.filter(obj => {
|
||||
return this.cleanedColumnsShow.show.indexOf(obj.prop) > -1
|
||||
})
|
||||
},
|
||||
generatePopoverColumns() {
|
||||
this.popoverColumns.totalColumnsList = this.totalColumns.map(obj => {
|
||||
return { prop: obj.prop, label: obj.label }
|
||||
})
|
||||
this.popoverColumns.currentCols = this.cleanedColumnsShow.show
|
||||
this.popoverColumns.minCols = this.cleanedColumnsShow.min
|
||||
this.$log.debug('Popover cols: ', this.popoverColumns)
|
||||
},
|
||||
handlePopoverColumnsChange({ columns, url }) {
|
||||
this.$log.debug('Columns change: ', columns)
|
||||
this.popoverColumns.currentCols = columns
|
||||
const _tableConfig = localStorage.getItem('tableConfig')
|
||||
? JSON.parse(localStorage.getItem('tableConfig'))
|
||||
: {}
|
||||
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
|
||||
_tableConfig[tableName] = {
|
||||
'showColumns': columns
|
||||
}
|
||||
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))
|
||||
this.filterShowColumns()
|
||||
},
|
||||
filterChange(filters) {
|
||||
const key = Object.keys(filters)[0]
|
||||
const attr = {}
|
||||
attr[key] = filters[key][0]
|
||||
this.filterTable(attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<DataZTree ref="dataztree" :setting="treeSetting">
|
||||
<slot slot="rMenu">
|
||||
<DataZTree ref="dataztree" :setting="treeSetting" class="data-z-tree" v-on="$listeners">
|
||||
<slot v-if="treeSetting.hasRightMenu" slot="rMenu">
|
||||
<li id="m_create" class="rmenu" tabindex="-1" @click="createTreeNode">
|
||||
<i class="fa fa-plus-square-o" /> {{ this.$t('tree.CreateNode') }}
|
||||
</li>
|
||||
@@ -55,7 +55,8 @@ export default {
|
||||
// beforeDrag
|
||||
// onDrag
|
||||
// beforeAsync: this.defaultCallback.bind(this, 'beforeAsync')
|
||||
}
|
||||
},
|
||||
hasRightMenu: true
|
||||
},
|
||||
currentNode: '',
|
||||
currentNodeId: ''
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
treeSetting() {
|
||||
this.$log.debug('Settings: ', this.setting)
|
||||
return _.merge(this.defaultSetting, this.setting)
|
||||
},
|
||||
zTree() {
|
||||
@@ -76,6 +78,10 @@ export default {
|
||||
$('body').unbind('mousedown')
|
||||
},
|
||||
methods: {
|
||||
refreshTree: function() {
|
||||
const refreshIconRef = $('#tree-refresh')
|
||||
refreshIconRef.click()
|
||||
},
|
||||
editTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
const currentNode = this.zTree.getSelectedNodes()[0]
|
||||
@@ -98,15 +104,19 @@ export default {
|
||||
if (this.setting.url.indexOf('?') !== -1) {
|
||||
combinator = '&'
|
||||
}
|
||||
let url = ''
|
||||
const query = Object.assign({}, this.$route.query)
|
||||
if (treeNode.meta.type === 'node') {
|
||||
this.currentNode = treeNode
|
||||
this.currentNodeId = treeNode.meta.node.id
|
||||
this.$route.query['node'] = this.currentNodeId
|
||||
this.$emit('urlChange', `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`)
|
||||
query['node'] = this.currentNodeId
|
||||
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`
|
||||
} else if (treeNode.meta.type === 'asset') {
|
||||
this.$route.query['asset'] = treeNode.meta.asset.id
|
||||
this.$emit('urlChange', `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`)
|
||||
query['asset'] = treeNode.meta.asset.id
|
||||
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`
|
||||
}
|
||||
this.$router.push({ query })
|
||||
this.$emit('urlChange', url)
|
||||
},
|
||||
removeTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
@@ -119,8 +129,9 @@ export default {
|
||||
).then(() => {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.zTree.removeNode(currentNode)
|
||||
this.refreshTree()
|
||||
}).catch(() => {
|
||||
// this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
|
||||
// this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
|
||||
})
|
||||
},
|
||||
onRename: function(event, treeId, treeNode, isCancel) {
|
||||
@@ -139,9 +150,7 @@ export default {
|
||||
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
|
||||
this.zTree.updateNode(treeNode)
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
|
||||
})
|
||||
}).finally(() => { this.refreshTree() })
|
||||
},
|
||||
onBodyMouseDown: function(event) {
|
||||
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
|
||||
@@ -159,6 +168,11 @@ export default {
|
||||
y -= (offset.top + scrollTop) / 3 - 10
|
||||
x += document.body.scrollLeft
|
||||
y += document.body.scrollTop + document.documentElement.scrollTop
|
||||
|
||||
if (y + $(`#${rMenuID} ul`).height() >= window.innerHeight) {
|
||||
y -= $(`#${rMenuID} ul`).height()
|
||||
}
|
||||
|
||||
this.rMenu.css({ 'top': y + 'px', 'left': x + 'px', 'visibility': 'visible' })
|
||||
$(`#${rMenuID} ul`).show()
|
||||
$('body').bind('mousedown', this.onBodyMouseDown)
|
||||
@@ -205,7 +219,7 @@ export default {
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
|
||||
})
|
||||
}).finally(() => this.refreshTree())
|
||||
},
|
||||
createTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
@@ -237,19 +251,21 @@ export default {
|
||||
})
|
||||
},
|
||||
refresh: function() {
|
||||
this.$axios.post(
|
||||
'/api/v1/assets/nodes/00000000-0000-0000-0000-000000000000/tasks/',
|
||||
{ action: 'refresh_cache' }
|
||||
)
|
||||
},
|
||||
getSelectedNodes: function() {
|
||||
return this.zTree.getSelectedNodes()
|
||||
},
|
||||
getNodes: function() {
|
||||
return this.zTree.getNodes()
|
||||
},
|
||||
selectNode: function(node) {
|
||||
return this.zTree.selectNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
<style scoped>
|
||||
.rmenu {
|
||||
font-size: 12px;
|
||||
padding: 0 16px;
|
||||
@@ -272,4 +288,8 @@ export default {
|
||||
.rmenu:hover{
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.data-z-tree >>> .fa {
|
||||
width: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
174
src/components/DataActions/index.vue
Normal file
174
src/components/DataActions/index.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div :class="grouped ? 'el-button-group' : 'el-button-ungroup'">
|
||||
<template v-for="action in iActions">
|
||||
<el-dropdown
|
||||
v-if="action.dropdown"
|
||||
v-show="action.dropdown.length > 0"
|
||||
:key="action.name"
|
||||
class="action-item"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
@command="handleDropdownCallback"
|
||||
>
|
||||
<el-button :size="size" v-bind="cleanButtonAction(action)">
|
||||
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="option in action.dropdown">
|
||||
<div v-if="option.group" :key="'group:'+option.name" class="dropdown-menu-title">
|
||||
{{ option.group }}
|
||||
</div>
|
||||
<el-dropdown-item
|
||||
:key="option.name"
|
||||
:command="[option, action]"
|
||||
v-bind="option"
|
||||
>
|
||||
{{ option.title }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<el-button
|
||||
v-else
|
||||
:key="action.name"
|
||||
:size="size"
|
||||
v-bind="cleanButtonAction(action)"
|
||||
class="action-item"
|
||||
@click="handleClick(action)"
|
||||
>
|
||||
<el-tooltip :disabled="!action.tip" :content="action.tip" placement="top">
|
||||
<span>
|
||||
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DataActions',
|
||||
props: {
|
||||
grouped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iActions() {
|
||||
return this.cleanActions(this.actions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDropdownCallback(command) {
|
||||
const [option, dropdown] = command
|
||||
const defaultCallback = () => this.$log.debug('No callback found: ', option, dropdown)
|
||||
let callback = option.callback
|
||||
if (!callback) {
|
||||
callback = dropdown.callback
|
||||
}
|
||||
if (!callback) {
|
||||
callback = defaultCallback
|
||||
}
|
||||
return callback(option)
|
||||
},
|
||||
handleClick(action) {
|
||||
if (action && action.callback) {
|
||||
action.callback(action)
|
||||
} else {
|
||||
this.$log.debug('No callback found')
|
||||
}
|
||||
this.$emit('actionClick', action)
|
||||
},
|
||||
checkItem(item, attr, defaults) {
|
||||
if (!item) {
|
||||
return true
|
||||
}
|
||||
let ok = item[attr]
|
||||
if (ok && typeof ok === 'function') {
|
||||
ok = ok(item)
|
||||
} else if (ok == null) {
|
||||
ok = defaults === undefined ? true : defaults
|
||||
}
|
||||
return ok
|
||||
},
|
||||
cleanButtonAction(action) {
|
||||
action = _.cloneDeep(action)
|
||||
delete action['dropdown']
|
||||
delete action['callback']
|
||||
delete action['name']
|
||||
delete action['can']
|
||||
return action
|
||||
},
|
||||
cleanActions(actions) {
|
||||
const cleanedActions = []
|
||||
const cloneActions = _.cloneDeep(actions)
|
||||
for (const v of cloneActions) {
|
||||
if (!v) {
|
||||
continue
|
||||
}
|
||||
const action = Object.assign({}, v)
|
||||
// 是否拥有这个action
|
||||
const has = this.checkItem(action, 'has')
|
||||
delete action['has']
|
||||
if (!has) {
|
||||
continue
|
||||
}
|
||||
// 是否有分割线
|
||||
action.divided = this.checkItem(action, 'divided', false)
|
||||
|
||||
// 是否是disabled
|
||||
const can = this.checkItem(action, 'can')
|
||||
action.disabled = !can
|
||||
|
||||
if (action.dropdown) {
|
||||
// const dropdown = this.cleanActions(action.dropdown)
|
||||
action.dropdown = this.cleanActions(action.dropdown)
|
||||
}
|
||||
cleanedActions.push(action)
|
||||
}
|
||||
return cleanedActions
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-menu-title {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 30px;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: solid 1px #e4e7ed;
|
||||
}
|
||||
|
||||
.dropdown-menu-title:first-child {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.el-button-ungroup .action-item {
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
.el-button-ungroup .action-item:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
:content="fields"
|
||||
:form="basicForm"
|
||||
label-position="right"
|
||||
label-width="17%"
|
||||
label-width="20%"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
@@ -15,6 +15,7 @@
|
||||
<el-form-item>
|
||||
<el-button v-for="button in moreButtons" :key="button.title" size="small" v-bind="button" @click="handleClick(button)">{{ button.title }}</el-button>
|
||||
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">{{ $t('common.Reset') }}</el-button>
|
||||
<el-button v-if="defaultButton && hasSaveContinue" size="small" @click="submitForm('form', true)">{{ $t('common.SaveAndAddAnother') }}</el-button>
|
||||
<el-button v-if="defaultButton" size="small" :loading="isSubmitting" type="primary" @click="submitForm('form')">{{ $t('common.Submit') }}</el-button>
|
||||
</el-form-item>
|
||||
</ElFormRender>
|
||||
@@ -35,6 +36,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasSaveContinue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -42,7 +47,7 @@ export default {
|
||||
// 初始值
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => { return {} }
|
||||
default: () => ({})
|
||||
},
|
||||
moreButtons: {
|
||||
type: Array,
|
||||
@@ -60,11 +65,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
// 获取表单数据
|
||||
submitForm(formName) {
|
||||
submitForm(formName, addContinue) {
|
||||
const form = this.$refs[formName]
|
||||
form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.$emit('submit', form.getFormValue(), form)
|
||||
this.$emit('submit', form.getFormValue(), form, addContinue)
|
||||
} else {
|
||||
this.$emit('invalid', valid)
|
||||
return false
|
||||
@@ -77,7 +82,7 @@ export default {
|
||||
},
|
||||
handleClick(button) {
|
||||
const callback = button.callback || function(values, form) {
|
||||
console.log('Click ', button.title, ': ', values)
|
||||
// console.log('Click ', button.title, ': ', values)
|
||||
}
|
||||
const form = this.$refs['form']
|
||||
const values = form.getFormValue()
|
||||
|
||||
@@ -8,7 +8,27 @@ export const RequiredChange = {
|
||||
required: true, message: i18n.t('common.fieldRequiredError'), trigger: 'change'
|
||||
}
|
||||
|
||||
export const EmailCheck = {
|
||||
type: 'email',
|
||||
message: i18n.t('common.InputEmailAddress'),
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
export default {
|
||||
Required,
|
||||
RequiredChange
|
||||
RequiredChange,
|
||||
EmailCheck
|
||||
}
|
||||
|
||||
export const JsonRequired = {
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
validator: (rule, value, callback) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (e) {
|
||||
callback(new Error(i18n.t('common.InvalidJson')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
v-bind="tableAttrs"
|
||||
:data="data"
|
||||
:row-class-name="rowClassName"
|
||||
v-on="$listeners"
|
||||
@selection-change="selectStrategy.onSelectionChange"
|
||||
@select="selectStrategy.onSelect"
|
||||
@select-all="selectStrategy.onSelectAll($event, canSelect)"
|
||||
@@ -95,6 +96,9 @@
|
||||
v-for="col in columns"
|
||||
:key="col.prop"
|
||||
:formatter="typeof col.formatter === 'function' ? col.formatter : null"
|
||||
:filters="col.filters || null"
|
||||
:filter-multiple="false"
|
||||
:filter-method="typeof col.filterMethod === 'function' ? col.filterMethod : null"
|
||||
v-bind="{align: columnsAlign, ...col}"
|
||||
>
|
||||
<template v-if="col.formatter && typeof col.formatter !== 'function'" v-slot:default="{row, column, index}">
|
||||
@@ -163,7 +167,7 @@ import getLocatedSlotKeys from './utils/extract-keys'
|
||||
import transformSearchImmediatelyItem from './utils/search-immediately-item'
|
||||
import isFalsey from './utils/is-falsey'
|
||||
import merge from 'deepmerge'
|
||||
const defaultFirstPage = 0
|
||||
const defaultFirstPage = 1
|
||||
const noPaginationDataPath = 'payload'
|
||||
|
||||
export default {
|
||||
@@ -422,7 +426,6 @@ export default {
|
||||
onEdit: {
|
||||
type: Function,
|
||||
default(row) {
|
||||
console.log('On delete row')
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -719,6 +722,10 @@ export default {
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
totalData: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -806,6 +813,13 @@ export default {
|
||||
},
|
||||
_searchForm() {
|
||||
return transformSearchImmediatelyItem(this.collapseForm, this)
|
||||
},
|
||||
lastPageNum() {
|
||||
// page
|
||||
const pageOffset = this.firstPage - defaultFirstPage
|
||||
const pageCount = Math.ceil(this.total / this.size)
|
||||
const lastPageNum = pageCount + pageOffset
|
||||
return lastPageNum
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -824,6 +838,13 @@ export default {
|
||||
* @property {array} rows - 已选中的行数据的数组
|
||||
*/
|
||||
this.$emit('selection-change', val)
|
||||
},
|
||||
totalData(val) {
|
||||
if (val) {
|
||||
this.page = defaultFirstPage
|
||||
this.total = val.length
|
||||
this.getList()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -840,6 +861,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.totalData) {
|
||||
this.getList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery() {
|
||||
@@ -873,12 +897,58 @@ export default {
|
||||
}
|
||||
return query
|
||||
},
|
||||
getPageData() {
|
||||
return this.data
|
||||
},
|
||||
async gotoNextPage() {
|
||||
if (!this.hasNextPage()) {
|
||||
return false
|
||||
}
|
||||
this.page += 1
|
||||
await this.getList({ loading: true })
|
||||
},
|
||||
hasNextPage() {
|
||||
return this.page < this.lastPageNum
|
||||
},
|
||||
getList({ loading = true } = {}) {
|
||||
const { url } = this
|
||||
if (url) {
|
||||
return this.getListFromRemote({ loading: loading })
|
||||
}
|
||||
if (this.totalData) {
|
||||
return this.getListFromStaticData({ loading: true })
|
||||
}
|
||||
// this.$log.debug("last page is: ", this.lastPageNum)
|
||||
},
|
||||
getListFromStaticData({ loading = true } = {}) {
|
||||
if (loading) {
|
||||
this.loading = true
|
||||
}
|
||||
if (!this.hasPagination) {
|
||||
this.data = this.totalData
|
||||
this.loading = false
|
||||
if (this.isTree) {
|
||||
this.data = this.tree2Array(this.data, this.expandAll)
|
||||
}
|
||||
return this.data
|
||||
}
|
||||
// page
|
||||
const pageOffset = this.firstPage - defaultFirstPage
|
||||
const page = this.page === 0 ? 1 : this.page
|
||||
const start = (page + pageOffset - 1) * this.size
|
||||
const end = (page + pageOffset) * this.size
|
||||
this.$log.debug(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
|
||||
this.data = this.totalData.slice(start, end)
|
||||
this.loading = false
|
||||
this.data = this.tree2Array(this.data, this.expandAll)
|
||||
return this.data
|
||||
},
|
||||
/**
|
||||
* 手动刷新列表数据,选项的默认值为: { loading: true }
|
||||
* @public
|
||||
* @param {object} options 方法选项
|
||||
*/
|
||||
getList({ loading = true } = {}) {
|
||||
getListFromRemote({ loading = true } = {}) {
|
||||
const { url } = this
|
||||
if (!url) {
|
||||
return
|
||||
@@ -1006,7 +1076,7 @@ export default {
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
if (this.size === val) return
|
||||
|
||||
this.$emit('sizeChange', val)
|
||||
this.page = defaultFirstPage
|
||||
this.size = val
|
||||
this.getList()
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<ElDatableTable ref="table" class="el-table" v-bind="tableConfig" @update="onUpdate" v-on="iListeners" />
|
||||
<ElDatableTable
|
||||
ref="table"
|
||||
class="el-table"
|
||||
v-bind="tableConfig"
|
||||
@update="onUpdate"
|
||||
v-on="iListeners"
|
||||
@sizeChange="handleSizeChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { default as ElDatableTable } from './compenents/el-data-table'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
@@ -58,7 +66,6 @@ export default {
|
||||
pageCount: 5,
|
||||
paginationLayout: 'total, sizes, prev, pager, next',
|
||||
paginationSizes: [15, 30, 50, 100],
|
||||
paginationSize: 15,
|
||||
paginationBackground: true,
|
||||
transformQuery: query => {
|
||||
if (query.page && query.size) {
|
||||
@@ -85,12 +92,20 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
tableConfig() {
|
||||
const config = Object.assign(this.defaultConfig, this.config)
|
||||
const tableDefaultConfig = this.defaultConfig
|
||||
tableDefaultConfig.paginationSize = _.get(this.globalTableConfig, 'paginationSize', 15)
|
||||
const config = Object.assign(tableDefaultConfig, this.config)
|
||||
return config
|
||||
},
|
||||
iListeners() {
|
||||
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
|
||||
}
|
||||
},
|
||||
dataTable() {
|
||||
return this.$refs.table
|
||||
},
|
||||
...mapGetters({
|
||||
'globalTableConfig': 'tableConfig'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
@@ -131,6 +146,14 @@ export default {
|
||||
this.toggleRowSelection(row, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.$store.commit('table/SET_TABLE_CONFIG',
|
||||
{
|
||||
key: 'paginationSize',
|
||||
value: val
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="treebox">
|
||||
<ul v-show="loading" class="ztree">
|
||||
{{ this.$t('common.tree.Loading') }}...
|
||||
</ul>
|
||||
<div v-show="!loading" class="treebox">
|
||||
<ul :id="iZTreeID" class="ztree">
|
||||
{{ this.$t('common.tree.Loading') }}...
|
||||
</ul>
|
||||
@@ -22,6 +25,7 @@
|
||||
import $ from '@/utils/jquery-vendor.js'
|
||||
import '@ztree/ztree_v3/js/jquery.ztree.all.min.js'
|
||||
import '@/styles/ztree.css'
|
||||
import axiosRetry from 'axios-retry'
|
||||
|
||||
const defaultObject = {
|
||||
type: Object,
|
||||
@@ -40,7 +44,8 @@ export default {
|
||||
iRMenuID: `rMenu_${this._uid}`,
|
||||
zTree: '',
|
||||
rMenu: '',
|
||||
init: false
|
||||
init: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -57,14 +62,26 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initTree: function() {
|
||||
const vm = this
|
||||
let treeUrl
|
||||
if (this.init) {
|
||||
this.loading = true
|
||||
}
|
||||
if (this.init && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 && this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1) {
|
||||
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1) ? `${this.treeSetting.treeUrl}?rebuild_tree=1` : `${this.treeSetting.treeUrl}&rebuild_tree=1`
|
||||
} else {
|
||||
treeUrl = this.treeSetting.treeUrl
|
||||
this.init = true
|
||||
}
|
||||
this.$axios.get(treeUrl).then(res => {
|
||||
this.$axios.get(treeUrl, {
|
||||
'axios-retry': {
|
||||
retries: 20,
|
||||
retryCondition: e => {
|
||||
return axiosRetry.isNetworkOrIdempotentRequestError(e) || e.response.status === 409
|
||||
},
|
||||
shouldResetTimeout: true,
|
||||
retryDelay: () => { return 5000 }
|
||||
}
|
||||
}).then(res => {
|
||||
if (!res) {
|
||||
res = []
|
||||
}
|
||||
@@ -74,7 +91,12 @@ export default {
|
||||
})
|
||||
}
|
||||
this.treeSetting.treeUrl = treeUrl
|
||||
if (this.init) {
|
||||
vm.zTree.destroy()
|
||||
}
|
||||
this.zTree = $.fn.zTree.init($(`#${this.iZTreeID}`), this.treeSetting, res)
|
||||
// 手动上报事件, Tree加载完成
|
||||
this.$emit('TreeInitFinish', this.zTree)
|
||||
if (this.treeSetting.showRefresh) {
|
||||
this.rootNodeAddDom(
|
||||
this.zTree,
|
||||
@@ -88,6 +110,9 @@ export default {
|
||||
if (this.treeSetting.otherMenu) {
|
||||
$('.menu-actions').append(this.otherMenu)
|
||||
}
|
||||
}).finally(_ => {
|
||||
vm.loading = false
|
||||
vm.init = true
|
||||
})
|
||||
},
|
||||
rootNodeAddDom: function(ztree, callback) {
|
||||
@@ -104,7 +129,6 @@ export default {
|
||||
}
|
||||
const refreshIconRef = $('#tree-refresh')
|
||||
refreshIconRef.bind('click', function() {
|
||||
ztree.destroy()
|
||||
const result = callback()
|
||||
if (result && result.then) {
|
||||
result.finally(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ZTree ref="ztree" :setting="treeSetting">
|
||||
<ZTree ref="ztree" :setting="treeSetting" v-on="$listeners">
|
||||
<!--Slot透传-->
|
||||
<div slot="rMenu" slot-scope="{data}">
|
||||
<slot name="rMenu" :data="data" />
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
defaultCallback: function(action) {
|
||||
console.log(action)
|
||||
// console.log(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -74,4 +73,8 @@ export default {
|
||||
/*padding-top: 10px;*/
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
30
src/components/FormFields/Link.vue
Normal file
30
src/components/FormFields/Link.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<el-link @click="onClick">
|
||||
{{ title }}
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Link',
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
window.open(this.href)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -6,7 +6,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordInput from '../PasswordInput'
|
||||
import PasswordInput from './PasswordInput'
|
||||
import { mapGetters } from 'vuex'
|
||||
import store from '@/store'
|
||||
import i18n from '@/i18n/i18n'
|
||||
250
src/components/FormFields/WeekCronSelect.vue
Normal file
250
src/components/FormFields/WeekCronSelect.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hours-container">
|
||||
<div v-for="(item, index) in hours" :key="index" class="hours-item">
|
||||
<div class="hours-item-header">{{ compItem(item) }}</div>
|
||||
<div class="hours-item-value">
|
||||
<div
|
||||
:class="compClass(2 * item)"
|
||||
@click="handleClick(2 * item)"
|
||||
@mouseover="handleHover(2 * item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips">{{ tips }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'sendTimeList'
|
||||
},
|
||||
props: {
|
||||
sendTimeList: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], // 选项
|
||||
selectStart: false, // 开始
|
||||
startIndex: '', // 开始下标
|
||||
timeRangeList: [], // 选择的时间段
|
||||
timeRangeListIndex: [], // 选中的下标
|
||||
tempRangeIndex: [], // 预选下标
|
||||
tips: '向右选中,向左取消选择'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
watch: {
|
||||
timeRangeList: function(value) {
|
||||
this.$emit('change', value)
|
||||
this.$parent.$emit('el.form.change')// 触发父组件的校验规则
|
||||
},
|
||||
sendTimeList: {
|
||||
handler() {
|
||||
this.transformedIndex()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.transformedIndex()
|
||||
},
|
||||
methods: {
|
||||
// 时间区间转换成下标区间
|
||||
transformedIndex() {
|
||||
this.timeRangeListIndex = []
|
||||
this.timeRangeList = this.sendTimeList
|
||||
this.timeRangeList.forEach(element => {
|
||||
const [startTime, endTime] = element.match(/\d+\:\d+/g)
|
||||
if (startTime && endTime) {
|
||||
const [startHour, startMin] = startTime.split(':')
|
||||
const [endHour, endMin] = endTime.split(':')
|
||||
if (startHour && startMin && endHour && endMin) {
|
||||
let startNum, endNum
|
||||
if (startMin === '00') {
|
||||
startNum = 2 * parseInt(startHour)
|
||||
} else {
|
||||
startNum = 2 * parseInt(startHour) + 1
|
||||
}
|
||||
if (endMin === '00') {
|
||||
endNum = 2 * parseInt(endHour) - 1
|
||||
} else {
|
||||
endNum = 2 * parseInt(endHour)
|
||||
}
|
||||
while (endNum >= startNum) {
|
||||
this.timeRangeListIndex.push(startNum)
|
||||
startNum++
|
||||
}
|
||||
} else {
|
||||
this.$message.error('时间段格式不正确')
|
||||
}
|
||||
} else {
|
||||
this.$message.error('没有拿到开始时间或结束时间或者时间段格式不对')
|
||||
}
|
||||
})
|
||||
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
|
||||
},
|
||||
// 下标区间转换成时间区间
|
||||
transformedSection() {
|
||||
this.timeRangeList = []
|
||||
let startTime = ''; let endTime = ''; const len = this.hours.length
|
||||
for (let index = this.hours[0] * 2; index < 2 * (len + 1); index++) {
|
||||
if (this.timeRangeListIndex.indexOf(index) > -1) {
|
||||
if (startTime) { // 如果有开始时间,直接确定结束时间
|
||||
const endHour = Math.floor((index + 1) / 2)
|
||||
const endMin = (index + 1) % 2 === 0 ? '00' : '30'
|
||||
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
|
||||
} else { // 没有开始时间,确定当前点为开始时间
|
||||
const startHour = Math.floor(index / 2)
|
||||
const startMin = index % 2 === 0 ? '00' : '30'
|
||||
startTime = `${startHour < 10 ? '0' + startHour : startHour}:${startMin}`
|
||||
}
|
||||
if (index === 2 * this.hours.length + 1) { // 如果是最后一格,直接结束
|
||||
endTime = `${Math.floor((index + 1) / 2)}:00`
|
||||
this.timeRangeList.push(`${startTime || '23:30'}-${endTime}`)
|
||||
startTime = ''
|
||||
endTime = ''
|
||||
}
|
||||
} else { // 若这个点不在选择区间,确定一个时间段
|
||||
if (startTime && endTime) {
|
||||
this.timeRangeList.push(`${startTime}-${endTime}`)
|
||||
startTime = ''
|
||||
endTime = ''
|
||||
} else if (startTime && !endTime) { // 这里可能只选半个小时
|
||||
const endHour = Math.floor(index / 2)
|
||||
const endMin = index % 2 === 0 ? '00' : '30'
|
||||
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
|
||||
this.timeRangeList.push(`${startTime}-${endTime}`)
|
||||
startTime = ''
|
||||
endTime = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
|
||||
},
|
||||
// 点击事件
|
||||
handleClick(index) {
|
||||
if (this.selectStart) {
|
||||
if (index === this.startIndex) { // 双击取反
|
||||
if (this.timeRangeListIndex.indexOf(index) > -1) {
|
||||
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
|
||||
} else {
|
||||
this.timeRangeListIndex.push(this.startIndex)
|
||||
}
|
||||
} else if (index > this.startIndex) { // 选取数据--向右添加,向左取消
|
||||
while (index >= this.startIndex) {
|
||||
this.timeRangeListIndex.push(this.startIndex)
|
||||
this.startIndex++
|
||||
}
|
||||
this.timeRangeListIndex = Array.from(new Set(this.timeRangeListIndex))
|
||||
} else { // 删除数据
|
||||
while (this.startIndex >= index) {
|
||||
if (this.timeRangeListIndex.indexOf(index) > -1) {
|
||||
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
this.startIndex = ''
|
||||
this.tempRangeIndex = []
|
||||
this.transformedSection()
|
||||
} else {
|
||||
this.startIndex = index
|
||||
}
|
||||
this.selectStart = !this.selectStart
|
||||
},
|
||||
// 预选区间
|
||||
handleHover(index) {
|
||||
if (this.selectStart) {
|
||||
this.tempRangeIndex = []
|
||||
if (index > this.startIndex) { // 选取数据--向右添加,向左取消
|
||||
while (index >= this.startIndex) {
|
||||
this.tempRangeIndex.push(index)
|
||||
index--
|
||||
}
|
||||
} else { // 删除数据
|
||||
while (this.startIndex >= index) {
|
||||
this.tempRangeIndex.push(index)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 是否选中,计算className
|
||||
compClass(index) {
|
||||
if (index === this.startIndex) {
|
||||
return 'hours-item-left preSelected'
|
||||
}
|
||||
if (index >= this.startIndex) {
|
||||
if (this.tempRangeIndex.indexOf(index) > -1) {
|
||||
return 'hours-item-left preSelected'
|
||||
}
|
||||
} else {
|
||||
if (this.tempRangeIndex.indexOf(index) > -1) {
|
||||
return 'hours-item-left unSelected'
|
||||
}
|
||||
}
|
||||
return this.timeRangeListIndex.indexOf(index) > -1 ? 'hours-item-left selected' : 'hours-item-left'
|
||||
},
|
||||
compItem(item) { // 不足10前面补0
|
||||
return item < 10 ? `0${item}` : item
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.hours-container {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
.hours-item {
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
border: 1px solid #c2d0f3;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
&:last-child {
|
||||
border-right: 1px solid #c2d0f3;
|
||||
}
|
||||
.hours-item-header {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-bottom: 1px solid #c2d0f3;
|
||||
}
|
||||
.hours-item-value {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
.selected {
|
||||
background-color: #4e84fe;
|
||||
border-bottom: 1px solid #c2d0f3;
|
||||
}
|
||||
.preSelected {
|
||||
background-color: #8eaffc;
|
||||
border-bottom: 1px solid #c2d0f3;
|
||||
}
|
||||
.unSelected {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #c2d0f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
width: 100%;
|
||||
line-height: 30px;
|
||||
}
|
||||
</style>
|
||||
33
src/components/FormFields/index.js
Normal file
33
src/components/FormFields/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import DatetimeRangePicker from './DatetimeRangePicker'
|
||||
import Link from './Link'
|
||||
import PasswordInput from './PasswordInput'
|
||||
import Select2 from './Select2'
|
||||
import Swicher from './Swicher'
|
||||
import UploadField from './UploadField'
|
||||
import UploadKey from './UploadKey'
|
||||
import UserPassword from './UserPassword'
|
||||
import WeekCronSelect from './WeekCronSelect'
|
||||
|
||||
export default {
|
||||
DatetimeRangePicker,
|
||||
Link,
|
||||
PasswordInput,
|
||||
Select2,
|
||||
Swicher,
|
||||
UploadKey,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
WeekCronSelect
|
||||
}
|
||||
|
||||
export {
|
||||
DatetimeRangePicker,
|
||||
Link,
|
||||
PasswordInput,
|
||||
Select2,
|
||||
Swicher,
|
||||
UploadKey,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
WeekCronSelect
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script type="text/jsx">
|
||||
import { DetailFormatter, SystemUserFormatter } from '@/components/ListTable/formatters'
|
||||
import { DetailFormatter, SystemUserFormatter } from '@/components/TableFormatters'
|
||||
import TreeTable from '../TreeTable'
|
||||
|
||||
export default {
|
||||
@@ -65,22 +65,24 @@ export default {
|
||||
sortable: true,
|
||||
formatterArgs: {
|
||||
route: 'AssetDetail'
|
||||
}
|
||||
},
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'ip',
|
||||
label: this.$t('assets.IP'),
|
||||
width: '140px',
|
||||
sortable: 'custom'
|
||||
},
|
||||
{
|
||||
prop: 'systemUsers',
|
||||
label: this.$t('assets.SystemUsers'),
|
||||
align: 'center',
|
||||
width: '200px',
|
||||
formatter: SystemUserFormatter,
|
||||
formatterArgs: {
|
||||
getUrl: this.getShowUrl.bind(this)
|
||||
}
|
||||
},
|
||||
showOverflowTooltip: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<Dialog v-if="showExportDialog" :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
|
||||
<el-form label-position="left" style="padding-left: 50px">
|
||||
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
|
||||
<el-radio-group v-model="exportTypeOption">
|
||||
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
|
||||
<el-radio-group v-model="exportOption">
|
||||
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
|
||||
@@ -52,6 +57,7 @@ export default {
|
||||
return {
|
||||
showExportDialog: false,
|
||||
exportOption: 'all',
|
||||
exportTypeOption: 'csv',
|
||||
meta: {}
|
||||
}
|
||||
},
|
||||
@@ -92,11 +98,27 @@ export default {
|
||||
can: this.tableHasQuery && this.canExportFiltered
|
||||
}
|
||||
]
|
||||
},
|
||||
exportTypeOptions() {
|
||||
return [
|
||||
{
|
||||
label: 'CSV',
|
||||
value: 'csv',
|
||||
can: true
|
||||
},
|
||||
{
|
||||
label: 'Excel',
|
||||
value: 'xlsx',
|
||||
can: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('showExportDialog', (row) => {
|
||||
this.showExportDialog = true
|
||||
this.$eventBus.$on('showExportDialog', ({ selectedRows, url }) => {
|
||||
if (url === this.url) {
|
||||
this.showExportDialog = true
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
@@ -123,7 +145,7 @@ export default {
|
||||
// delete query['limit']
|
||||
// delete query['offset']
|
||||
}
|
||||
query['format'] = 'csv'
|
||||
query['format'] = this.exportTypeOption
|
||||
const queryStr =
|
||||
(url.indexOf('?') > -1 ? '&' : '?') +
|
||||
queryUtil.stringify(query, '=', '&')
|
||||
|
||||
@@ -1,59 +1,73 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:title="$t('common.Import')"
|
||||
:title="importTitle"
|
||||
:visible.sync="showImportDialog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:loading-status="loadStatus"
|
||||
@confirm="handleImportConfirm"
|
||||
@cancel="handleImportCancel()"
|
||||
width="80%"
|
||||
class="importDialog"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
@close="handleImportCancel"
|
||||
>
|
||||
<el-form label-position="left" style="padding-left: 50px">
|
||||
<el-form v-if="!showTable" label-position="left" style="padding-left: 50px">
|
||||
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
|
||||
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
|
||||
<el-radio v-model="importOption" class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
|
||||
<el-radio v-model="importOption" class="export-item" label="create">{{ this.$t('common.Create') }}</el-radio>
|
||||
<el-radio v-model="importOption" class="export-item" label="update">{{ this.$t('common.Update') }}</el-radio>
|
||||
<div style="line-height: 1.5">
|
||||
<span v-if="importOption==='1'" class="el-upload__tip">
|
||||
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
|
||||
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
|
||||
</span>
|
||||
<span v-else class="el-upload__tip">
|
||||
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
|
||||
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
|
||||
<span class="el-upload__tip">
|
||||
{{ downloadTemplateTitle }}
|
||||
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
|
||||
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
|
||||
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'" class="file-uploader">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
drag
|
||||
action="string"
|
||||
list-type="text/csv"
|
||||
:http-request="handleImport"
|
||||
:limit="1"
|
||||
:auto-upload="false"
|
||||
:on-change="onFileChange"
|
||||
:before-upload="beforeUpload"
|
||||
accept=".csv,.xlsx"
|
||||
>
|
||||
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
|
||||
<div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>
|
||||
<i class="el-icon-upload" />
|
||||
<div class="el-upload__text">{{ $t('common.imExport.dragUploadFileInfo') }}</div>
|
||||
<div slot="tip" class="el-upload__tip">
|
||||
<span :class="{'hasError': hasFileFormatOrSizeError }">{{ $t('common.imExport.uploadCsvLth10MHelpText') }}</span>
|
||||
<div v-if="renderError" class="hasError">{{ renderError }}</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="errorMsg" class="error-msg error-results">
|
||||
<ul v-if="typeof errorMsg === 'object'">
|
||||
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
|
||||
</ul>
|
||||
<span v-else>{{ errorMsg }}</span>
|
||||
<div v-else class="importTableZone">
|
||||
<ImportTable
|
||||
ref="importTable"
|
||||
:json-data="jsonData"
|
||||
:import-option="importOption"
|
||||
:url="url"
|
||||
@cancel="cancelUpload"
|
||||
@finish="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import ImportTable from '@/components/ListTable/TableAction/ImportTable'
|
||||
import { getErrorResponseMsg } from '@/utils/common'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
|
||||
export default {
|
||||
name: 'ImportDialog',
|
||||
components: {
|
||||
Dialog
|
||||
Dialog,
|
||||
ImportTable
|
||||
},
|
||||
props: {
|
||||
selectedRows: {
|
||||
@@ -68,87 +82,119 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showImportDialog: false,
|
||||
importOption: '1',
|
||||
isCsv: true,
|
||||
importOption: 'create',
|
||||
errorMsg: '',
|
||||
loadStatus: false
|
||||
loadStatus: false,
|
||||
importTypeOption: 'csv',
|
||||
importTypeIsCsv: true,
|
||||
showTable: false,
|
||||
renderError: '',
|
||||
hasFileFormatOrSizeError: false,
|
||||
jsonData: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasSelected() {
|
||||
return this.selectedRows.length > 0
|
||||
},
|
||||
upLoadUrl() {
|
||||
return this.url
|
||||
},
|
||||
downloadImportTempUrl() {
|
||||
const url = (this.url.indexOf('?') === -1) ? `${this.url}?format=csv&template=import&limit=1` : `${this.url}&format=csv&template=import&limit=1`
|
||||
return url
|
||||
},
|
||||
uploadHelpTextClass() {
|
||||
const cls = ['el-upload__tip']
|
||||
if (!this.isCsv) {
|
||||
cls.push('error-msg')
|
||||
}
|
||||
return cls
|
||||
},
|
||||
downloadTemplateTitle() {
|
||||
if (this.importOption === 'create') {
|
||||
return this.$t('common.imExport.downloadImportTemplateMsg')
|
||||
} else {
|
||||
return this.$t('common.imExport.downloadUpdateTemplateMsg')
|
||||
}
|
||||
},
|
||||
importTitle() {
|
||||
if (this.importOption === 'create') {
|
||||
return this.$t('common.Import') + this.$t('common.Create')
|
||||
} else {
|
||||
return this.$t('common.Import') + this.$t('common.Update')
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
importOption(val) {
|
||||
this.showTable = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('showImportDialog', (row) => {
|
||||
this.showImportDialog = true
|
||||
this.$eventBus.$on('showImportDialog', ({ url }) => {
|
||||
if (url === this.url) {
|
||||
this.showImportDialog = true
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
performUpdate(item) {
|
||||
this.$axios.put(
|
||||
this.upLoadUrl,
|
||||
item.file,
|
||||
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
|
||||
).then((data) => {
|
||||
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
|
||||
this.onSuccess(msg)
|
||||
closeDialog() {
|
||||
this.showImportDialog = false
|
||||
},
|
||||
cancelUpload() {
|
||||
this.showTable = false
|
||||
this.renderError = ''
|
||||
this.jsonData = {}
|
||||
},
|
||||
onFileChange(file, fileList) {
|
||||
fileList.splice(0, fileList.length)
|
||||
if (file.status !== 'ready') {
|
||||
return
|
||||
}
|
||||
// const isCsv = file.raw.type = 'text/csv'
|
||||
if (!this.beforeUpload(file)) {
|
||||
return
|
||||
}
|
||||
const isCsv = file.name.indexOf('csv') > -1
|
||||
const url = new URL(this.url, 'http://localhost')
|
||||
url.pathname += 'render-to-json/'
|
||||
const renderToJsonUrl = url.toString().replace('http://localhost', '')
|
||||
this.$axios.post(
|
||||
renderToJsonUrl,
|
||||
file.raw,
|
||||
{ headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
|
||||
).then(data => {
|
||||
this.jsonData = data
|
||||
this.showTable = true
|
||||
}).catch(error => {
|
||||
this.catchError(error)
|
||||
fileList.splice(0, fileList.length)
|
||||
this.renderError = getErrorResponseMsg(error)
|
||||
}).finally(() => {
|
||||
this.loadStatus = false
|
||||
})
|
||||
},
|
||||
performCreate(item) {
|
||||
this.$axios.post(
|
||||
this.upLoadUrl,
|
||||
item.file,
|
||||
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
|
||||
).then((data) => {
|
||||
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
|
||||
this.onSuccess(msg)
|
||||
}).catch(error => {
|
||||
this.catchError(error)
|
||||
}).finally(() => {
|
||||
this.loadStatus = false
|
||||
})
|
||||
beforeUpload(file) {
|
||||
const isLt30M = file.size / 1024 / 1024 < 30
|
||||
if (!isLt30M) {
|
||||
this.hasFileFormatOrSizeError = true
|
||||
}
|
||||
return isLt30M
|
||||
},
|
||||
async downloadTemplateFile(tp) {
|
||||
const downloadUrl = await this.getDownloadTemplateUrl(tp)
|
||||
window.open(downloadUrl)
|
||||
},
|
||||
async getDownloadTemplateUrl(tp) {
|
||||
const template = this.importOption === 'create' ? 'import' : 'update'
|
||||
let query = `format=${tp}&template=${template}`
|
||||
if (this.importOption === 'update' && this.selectedRows.length > 0) {
|
||||
const resources = []
|
||||
for (const item of this.selectedRows) {
|
||||
resources.push(item.id)
|
||||
}
|
||||
const resp = await createSourceIdCache(resources)
|
||||
query += `&spm=${resp.spm}`
|
||||
} else {
|
||||
query += '&limit=1'
|
||||
}
|
||||
return this.url.indexOf('?') === -1 ? `${this.url}?${query}` : `${this.url}&${query}`
|
||||
},
|
||||
catchError(error) {
|
||||
this.$refs.upload.clearFiles()
|
||||
if (error.response && error.response.status === 400) {
|
||||
const errorData = error.response.data
|
||||
const totalErrorMsg = []
|
||||
errorData.forEach((value, index) => {
|
||||
if (typeof value === 'string') {
|
||||
totalErrorMsg.push(`line ${index}. ${value}`)
|
||||
} else {
|
||||
const errorMsg = [`line ${index}. `]
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (v) {
|
||||
errorMsg.push(`${k}: ${v}`)
|
||||
}
|
||||
}
|
||||
if (errorMsg.length > 1) {
|
||||
totalErrorMsg.push(errorMsg.join(' '))
|
||||
}
|
||||
}
|
||||
})
|
||||
this.errorMsg = totalErrorMsg
|
||||
}
|
||||
console.log(error)
|
||||
},
|
||||
onSuccess(msg) {
|
||||
this.errorMsg = ''
|
||||
@@ -160,34 +206,14 @@ export default {
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
handleImport(item) {
|
||||
this.loadStatus = true
|
||||
if (this.importOption === '1') {
|
||||
this.performCreate(item)
|
||||
} else {
|
||||
this.performUpdate(item)
|
||||
}
|
||||
},
|
||||
async downloadUpdateTempUrl() {
|
||||
var resources = []
|
||||
const data = this.selectedRows
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
resources.push(data[index].id)
|
||||
}
|
||||
const spm = await createSourceIdCache(resources)
|
||||
const baseUrl = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
|
||||
const url = `${baseUrl}?format=csv&template=update&spm=` + spm.spm
|
||||
return this.downloadCsv(url)
|
||||
},
|
||||
async handleImportConfirm() {
|
||||
this.$refs.upload.submit()
|
||||
this.$refs['importTable'].performUpload()
|
||||
},
|
||||
handleImportCancel() {
|
||||
this.showImportDialog = false
|
||||
},
|
||||
beforeUpload(file) {
|
||||
this.isCsv = _.endsWith(file.name, 'csv')
|
||||
return this.isCsv
|
||||
this.showTable = false
|
||||
this.renderError = ''
|
||||
this.jsonData = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,4 +230,49 @@ export default {
|
||||
overflow: auto
|
||||
}
|
||||
|
||||
.importDialog >>> .el-form-item.file-uploader {
|
||||
padding-right: 150px;
|
||||
}
|
||||
|
||||
.file-uploader >>> .el-upload {
|
||||
width: 100%;
|
||||
//padding-right: 150px;
|
||||
}
|
||||
|
||||
.file-uploader >>> .el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.importTableZone {
|
||||
padding: 0 20px;
|
||||
|
||||
.importTable {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tableFilter {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.importTable >>> .el-dialog__body {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.export-item {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.export-item:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
color: $--color-danger;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
line-height: 1.5;
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
426
src/components/ListTable/TableAction/ImportTable.vue
Normal file
426
src/components/ListTable/TableAction/ImportTable.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<div class="tableFilter">
|
||||
<el-radio-group v-model="importStatusFilter" size="small">
|
||||
<el-radio-button label="all">{{ $t('common.Total') }}</el-radio-button>
|
||||
<el-radio-button label="ok">{{ $t('common.Success') }}</el-radio-button>
|
||||
<el-radio-button label="error">{{ $t('common.Failed') }}</el-radio-button>
|
||||
<el-radio-button label="pending">{{ $t('common.Pending') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8" style="text-align: center">
|
||||
<span class="summary-item summary-total"> {{ $t('common.Total') }}: {{ totalCount }}</span>
|
||||
<span class="summary-item summary-success"> {{ $t('common.Success') }}: {{ successCount }}</span>
|
||||
<span class="summary-item summary-failed"> {{ $t('common.Failed') }}: {{ failedCount }}</span>
|
||||
<span class="summary-item summary-pending"> {{ $t('common.Pending') }}: {{ pendingCount }}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="row">
|
||||
<el-progress :percentage="processedPercent" />
|
||||
</div>
|
||||
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div style="float: right">
|
||||
<el-button size="small" @click="performCancel">{{ $t('common.Cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="performImportAction">{{ importActionTitle }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from '@/components/DataTable'
|
||||
import { sleep } from '@/utils/common'
|
||||
import { EditableInputFormatter, StatusFormatter } from '@/components/TableFormatters'
|
||||
export default {
|
||||
name: 'ImportTable',
|
||||
components: {
|
||||
DataTable
|
||||
},
|
||||
props: {
|
||||
jsonData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
importOption: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columns: [],
|
||||
importStatusFilter: 'all',
|
||||
iTotalData: [],
|
||||
tableConfig: {
|
||||
hasSelection: false,
|
||||
// hasPagination: false,
|
||||
columns: [],
|
||||
totalData: [],
|
||||
paginationSize: 10,
|
||||
paginationSizes: [10],
|
||||
tableAttrs: {
|
||||
stripe: true, // 斑马纹表格
|
||||
border: true, // 表格边框
|
||||
fit: true, // 宽度自适应,
|
||||
tooltipEffect: 'dark'
|
||||
}
|
||||
},
|
||||
tableGenDone: false,
|
||||
importTaskStatus: 'pending', // pending, started, stopped, done
|
||||
importTaskResult: '', // success, hasError
|
||||
hasImport: false,
|
||||
hasContinueButton: false,
|
||||
importActions: {
|
||||
import: this.$t('common.Import'),
|
||||
continue: this.$t('common.Continue'),
|
||||
stop: this.$t('common.Stop'),
|
||||
finished: this.$t('common.Finished')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tableColumnNameMapper() {
|
||||
const mapper = {}
|
||||
for (const column of this.tableConfig.columns) {
|
||||
mapper[column['prop']] = column['label']
|
||||
}
|
||||
return mapper
|
||||
},
|
||||
importAction() {
|
||||
switch (this.importTaskStatus) {
|
||||
case 'pending':
|
||||
return 'import'
|
||||
case 'started':
|
||||
return 'stop'
|
||||
}
|
||||
if (this.totalCount === this.successCount) {
|
||||
return 'finished'
|
||||
} else {
|
||||
return 'continue'
|
||||
}
|
||||
},
|
||||
importActionTitle() {
|
||||
return this.importActions[this.importAction]
|
||||
},
|
||||
successData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return item['@status'] === 'ok'
|
||||
})
|
||||
},
|
||||
failedData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
|
||||
})
|
||||
},
|
||||
pendingData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return item['@status'] === 'pending'
|
||||
})
|
||||
},
|
||||
totalCount() {
|
||||
return this.iTotalData.length
|
||||
},
|
||||
successCount() {
|
||||
return this.successData.length
|
||||
},
|
||||
failedCount() {
|
||||
return this.failedData.length
|
||||
},
|
||||
pendingCount() {
|
||||
return this.pendingData.length
|
||||
},
|
||||
processedCount() {
|
||||
return this.totalCount - this.pendingCount
|
||||
},
|
||||
processedPercent() {
|
||||
if (this.totalCount === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(this.processedCount / this.totalCount * 100)
|
||||
},
|
||||
elDataTable() {
|
||||
return this.$refs['dataTable'].dataTable
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
importStatusFilter(val) {
|
||||
if (val === 'all') {
|
||||
this.tableConfig.totalData = this.iTotalData
|
||||
} else if (val === 'error') {
|
||||
this.tableConfig.totalData = this.failedData
|
||||
} else {
|
||||
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
||||
return item['@status'] === val
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.generateTable()
|
||||
},
|
||||
methods: {
|
||||
generateTableColumns(tableTitles, tableData) {
|
||||
const vm = this
|
||||
const columns = [{
|
||||
prop: '@status',
|
||||
label: vm.$t('common.Status'),
|
||||
width: '80px',
|
||||
align: 'center',
|
||||
formatter: StatusFormatter,
|
||||
formatterArgs: {
|
||||
iconChoices: {
|
||||
ok: 'fa-check text-primary',
|
||||
error: 'fa-times text-danger',
|
||||
pending: 'fa-clock-o'
|
||||
},
|
||||
getChoicesKey(val) {
|
||||
if (val === 'ok' || val === 'pending') {
|
||||
return val
|
||||
}
|
||||
return 'error'
|
||||
},
|
||||
getTip(val) {
|
||||
if (val === 'ok') {
|
||||
return vm.$t('common.Success')
|
||||
} else if (val === 'pending') {
|
||||
return vm.$t('common.Pending')
|
||||
} else if (val && val.name === 'error') {
|
||||
return val.error
|
||||
}
|
||||
return ''
|
||||
},
|
||||
hasTips: true
|
||||
}
|
||||
}]
|
||||
for (const item of tableTitles) {
|
||||
const dataItemLens = tableData.map(d => {
|
||||
const prop = item[1]
|
||||
const itemColData = d[prop]
|
||||
if (!d) {
|
||||
return 0
|
||||
}
|
||||
if (typeof itemColData !== 'number' && (!itemColData || !itemColData.length)) {
|
||||
return 0
|
||||
}
|
||||
return itemColData.length
|
||||
})
|
||||
let colMaxWidth = Math.max(...dataItemLens) * 10
|
||||
if (colMaxWidth === 0) {
|
||||
continue
|
||||
}
|
||||
colMaxWidth = Math.min(180, colMaxWidth)
|
||||
colMaxWidth = Math.max(colMaxWidth, 100)
|
||||
columns.push({
|
||||
prop: item[1],
|
||||
label: item[0],
|
||||
minWidth: colMaxWidth + 'px',
|
||||
showOverflowTooltip: true,
|
||||
formatter: EditableInputFormatter,
|
||||
formatterArgs: {
|
||||
onEnter: ({ row, col, oldValue, newValue }) => {
|
||||
const prop = col.prop
|
||||
row['@status'] = 'pending'
|
||||
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
|
||||
this.$set(row, prop, newValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return columns
|
||||
},
|
||||
generateTableData(tableTitles, tableData) {
|
||||
const totalData = []
|
||||
tableData.forEach(item => {
|
||||
this.$set(item, '@status', 'pending')
|
||||
totalData.push(item)
|
||||
})
|
||||
return totalData
|
||||
},
|
||||
generateTable() {
|
||||
const tableTitles = this.jsonData['title']
|
||||
const tableData = this.jsonData['data']
|
||||
const columns = this.generateTableColumns(tableTitles, tableData)
|
||||
const totalData = this.generateTableData(tableTitles, tableData)
|
||||
this.tableConfig.columns = columns
|
||||
this.tableGenDone = true
|
||||
setTimeout(() => {
|
||||
this.iTotalData = totalData
|
||||
this.tableConfig.totalData = totalData
|
||||
}, 200)
|
||||
},
|
||||
beautifyErrorData(errorData) {
|
||||
if (typeof errorData === 'string') {
|
||||
return errorData
|
||||
} else if (Array.isArray(errorData)) {
|
||||
return errorData
|
||||
} else if (typeof errorData !== 'object') {
|
||||
return errorData
|
||||
}
|
||||
const data = []
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let [key, value] of Object.entries(errorData)) {
|
||||
if (typeof value === 'object') {
|
||||
value = this.beautifyErrorData(value)
|
||||
}
|
||||
let label = this.tableColumnNameMapper[key]
|
||||
if (!label) {
|
||||
label = key
|
||||
}
|
||||
data.push(`${label}: ${value}`)
|
||||
}
|
||||
return data
|
||||
},
|
||||
performCancel() {
|
||||
this.performStop()
|
||||
this.$emit('cancel')
|
||||
},
|
||||
performFinish() {
|
||||
this.performStop()
|
||||
this.$emit('finish')
|
||||
},
|
||||
taskIsStopped() {
|
||||
return this.importTaskStatus === 'stopped'
|
||||
},
|
||||
performImportAction() {
|
||||
switch (this.importAction) {
|
||||
case 'continue':
|
||||
return this.performContinue()
|
||||
case 'import':
|
||||
return this.performUpload()
|
||||
case 'stop':
|
||||
return this.performStop()
|
||||
case 'finished':
|
||||
return this.performFinish()
|
||||
}
|
||||
},
|
||||
performContinue() {
|
||||
if (this.importTaskStatus === 'done') {
|
||||
for (const item of this.failedData) {
|
||||
item['@status'] = 'pending'
|
||||
}
|
||||
this.tableConfig.totalData = this.pendingData
|
||||
}
|
||||
this.importTaskStatus = 'started'
|
||||
setTimeout(() => {
|
||||
this.performUpload()
|
||||
}, 100)
|
||||
},
|
||||
performStop() {
|
||||
this.importTaskStatus = 'stopped'
|
||||
},
|
||||
async performUploadCurrentPageData() {
|
||||
const currentData = this.elDataTable.getPageData()
|
||||
for (const item of currentData) {
|
||||
if (item['@status'] !== 'pending') {
|
||||
continue
|
||||
}
|
||||
if (this.taskIsStopped()) {
|
||||
return
|
||||
}
|
||||
await this.performUploadObject(item)
|
||||
await sleep(100)
|
||||
}
|
||||
},
|
||||
async performUpload() {
|
||||
this.importTaskStatus = 'started'
|
||||
this.importStatusFilter = 'pending'
|
||||
while (!this.taskIsStopped()) {
|
||||
await this.performUploadCurrentPageData()
|
||||
const hasNextPage = this.elDataTable.hasNextPage()
|
||||
if (hasNextPage && !this.taskIsStopped()) {
|
||||
await this.elDataTable.gotoNextPage()
|
||||
await sleep(100)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.pendingCount === 0) {
|
||||
this.importTaskStatus = 'done'
|
||||
}
|
||||
if (this.failedCount > 0) {
|
||||
this.$message.error(this.$t('common.imExport.hasImportErrorItemMsg') + '')
|
||||
}
|
||||
},
|
||||
async performUpdateObject(item) {
|
||||
const updateUrl = `${this.url}${item.id}/`
|
||||
return this.$axios.put(
|
||||
updateUrl,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
},
|
||||
async performUploadObject(item) {
|
||||
let handler = this.performCreateObject
|
||||
if (this.importOption === 'update') {
|
||||
handler = this.performUpdateObject
|
||||
}
|
||||
try {
|
||||
await handler.bind(this)(item)
|
||||
item['@status'] = 'ok'
|
||||
} catch (error) {
|
||||
const errorData = error?.response?.data
|
||||
const _error = this.beautifyErrorData(errorData)
|
||||
item['@status'] = {
|
||||
name: 'error',
|
||||
error: _error
|
||||
}
|
||||
}
|
||||
},
|
||||
async performCreateObject(item) {
|
||||
return this.$axios.post(
|
||||
this.url,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
},
|
||||
keepElementInViewport() {
|
||||
const tableRef = document.getElementById('importTable')
|
||||
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
|
||||
if (!pendingRef) {
|
||||
return
|
||||
}
|
||||
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
|
||||
const rect = parentTdRef.getBoundingClientRect()
|
||||
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
|
||||
windowInnerHeight = windowInnerHeight * 0.97 - 150
|
||||
const inViewport = (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= windowInnerHeight
|
||||
)
|
||||
if (!inViewport) {
|
||||
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/element-variables.scss";
|
||||
.summary-item {
|
||||
padding: 0 10px
|
||||
}
|
||||
|
||||
.summary-success {
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.summary-failed {
|
||||
color: $--color-danger;
|
||||
}
|
||||
|
||||
.importTable >>> .cell {
|
||||
min-height: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,30 +1,38 @@
|
||||
<template>
|
||||
<ActionsGroup v-if="hasLeftActions" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" v-bind="$attrs" class="header-action" />
|
||||
<DataActions
|
||||
v-if="hasLeftActions"
|
||||
:actions="iActions"
|
||||
v-bind="$attrs"
|
||||
class="header-action"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionsGroup from '@/components/ActionsGroup'
|
||||
import i18n from '@/i18n/i18n'
|
||||
import DataActions from '@/components/DataActions'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import { cleanActions } from './utils'
|
||||
|
||||
const defaultTrue = { type: Boolean, default: true }
|
||||
const defaultFalse = { type: Boolean, default: false }
|
||||
const defaultTrue = { type: [Boolean, Function], default: true }
|
||||
const defaultFalse = { type: [Boolean, Function], default: false }
|
||||
export default {
|
||||
name: 'LeftSide',
|
||||
components: {
|
||||
ActionsGroup
|
||||
DataActions
|
||||
},
|
||||
props: {
|
||||
hasLeftActions: defaultTrue,
|
||||
hasCreate: defaultTrue,
|
||||
canCreate: defaultTrue,
|
||||
hasBulkDelete: defaultTrue,
|
||||
hasBulkUpdate: defaultFalse,
|
||||
hasLeftActions: defaultTrue,
|
||||
hasMoreActions: defaultTrue,
|
||||
tableUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
createRoute: {
|
||||
type: [String, Object],
|
||||
type: [String, Object, Function],
|
||||
default: function() {
|
||||
return this.$route.name.replace('List', 'Create')
|
||||
}
|
||||
@@ -53,23 +61,41 @@ export default {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
moreActionsButton: {
|
||||
moreCreates: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
default: null
|
||||
},
|
||||
createTitle: {
|
||||
type: String,
|
||||
default: () => i18n.t('common.Create')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const defaultActions = [
|
||||
{
|
||||
name: 'actionCreate',
|
||||
title: this.createTitle,
|
||||
type: 'primary',
|
||||
has: this.hasCreate && !this.moreCreates,
|
||||
can: this.canCreate,
|
||||
callback: this.handleCreate
|
||||
}
|
||||
]
|
||||
if (this.moreCreates) {
|
||||
const defaultMoreCreate = {
|
||||
name: 'actionMoreCreate',
|
||||
title: this.createTitle,
|
||||
type: 'primary',
|
||||
has: true,
|
||||
can: this.canCreate,
|
||||
dropdown: [],
|
||||
callback: this.handleCreate
|
||||
}
|
||||
const createCreateAction = Object.assign(defaultMoreCreate, this.moreCreates)
|
||||
defaultActions.push(createCreateAction)
|
||||
}
|
||||
return {
|
||||
defaultActions: [
|
||||
{
|
||||
name: 'actionCreate',
|
||||
title: this.$t('common.Create'),
|
||||
type: 'primary',
|
||||
has: this.hasCreate,
|
||||
can: true,
|
||||
callback: this.handleCreate
|
||||
}
|
||||
],
|
||||
defaultActions: defaultActions,
|
||||
defaultMoreActions: [
|
||||
{
|
||||
title: this.$t('common.deleteSelected'),
|
||||
@@ -92,6 +118,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iActions() {
|
||||
return [...this.actions, this.moreAction]
|
||||
},
|
||||
actions() {
|
||||
const actions = [...this.defaultActions, ...this.extraActions]
|
||||
return cleanActions(actions, true, {
|
||||
@@ -99,12 +128,20 @@ export default {
|
||||
reloadTable: this.reloadTable
|
||||
})
|
||||
},
|
||||
moreActions() {
|
||||
const actions = [...this.defaultMoreActions, ...this.extraMoreActions]
|
||||
return cleanActions(actions, true, {
|
||||
moreAction() {
|
||||
if (!this.hasMoreActions) {
|
||||
return
|
||||
}
|
||||
let dropdown = [...this.defaultMoreActions, ...this.extraMoreActions]
|
||||
dropdown = cleanActions(dropdown, true, {
|
||||
selectedRows: this.selectedRows,
|
||||
reloadTable: this.reloadTable
|
||||
})
|
||||
return {
|
||||
name: 'moreActions',
|
||||
title: this.moreActionsTitle || this.$t('common.MoreActions'),
|
||||
dropdown: dropdown
|
||||
}
|
||||
},
|
||||
hasSelectedRows() {
|
||||
return this.selectedRows.length > 0
|
||||
@@ -112,10 +149,13 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleCreate() {
|
||||
let route = {}
|
||||
let route
|
||||
if (typeof this.createRoute === 'string') {
|
||||
route = { name: this.createRoute }
|
||||
route.name = this.createRoute
|
||||
} else {
|
||||
} else if (typeof this.createRoute === 'function') {
|
||||
route = this.createRoute()
|
||||
} else if (typeof this.createRoute === 'object') {
|
||||
route = this.createRoute
|
||||
}
|
||||
this.$router.push(route)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
|
||||
<ActionsGroup :is-fa="true" :actions="rightSideActions" :url="tableUrl" class="right-side-actions right-side-item" />
|
||||
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,14 +27,21 @@ export default {
|
||||
handleExport: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showExportDialog', { selectedRows })
|
||||
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl })
|
||||
}
|
||||
},
|
||||
hasImport: defaultTrue,
|
||||
handleImport: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showImportDialog', { selectedRows })
|
||||
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl })
|
||||
}
|
||||
},
|
||||
hasColumnSetting: defaultTrue,
|
||||
handleColumnConfig: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl })
|
||||
}
|
||||
},
|
||||
hasRefresh: defaultTrue,
|
||||
@@ -54,6 +61,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
defaultRightSideActions: [
|
||||
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleColumnConfig.bind(this) },
|
||||
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExport.bind(this) },
|
||||
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImport.bind(this) },
|
||||
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<script>
|
||||
import AutoDataSearch from '@/components/AutoDataSearch'
|
||||
import LeftSide from './LeftSide'
|
||||
import DatetimeRangePicker from '@/components/DatetimeRangePicker'
|
||||
import DatetimeRangePicker from '@/components/FormFields/DatetimeRangePicker'
|
||||
import RightSide from './RightSide'
|
||||
|
||||
const defaultTrue = { type: Boolean, default: true }
|
||||
|
||||
@@ -5,7 +5,9 @@ export function cleanActions(actions, canDefaults, { selectedRows, reloadTable }
|
||||
cloneActions.forEach((action) => {
|
||||
action.has = cleanBoolean(action, 'has', true, { selectedRows, reloadTable })
|
||||
action.can = cleanBoolean(action, 'can', true, { selectedRows, reloadTable })
|
||||
action.callback = cleanCallback(action, { selectedRows, reloadTable })
|
||||
if (!action.dropdown) {
|
||||
action.callback = cleanCallback(action, { selectedRows, reloadTable })
|
||||
}
|
||||
cleanedActions.push(action)
|
||||
})
|
||||
return cleanedActions
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<ActionsGroup :size="'mini'" :actions="cleanedActions" :more-actions="cleanMoreActions" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionsGroup from '@/components/ActionsGroup/index'
|
||||
import BaseFormatter from './base'
|
||||
|
||||
export default {
|
||||
name: 'CustomActionsFormatterVue',
|
||||
components: {
|
||||
ActionsGroup
|
||||
},
|
||||
extends: BaseFormatter,
|
||||
computed: {
|
||||
cleanedActions() {
|
||||
if (this.col.actions.actions instanceof Array) {
|
||||
const copy = _.cloneDeep(this.col.actions.actions)
|
||||
let actions = [...copy]
|
||||
actions = actions.map((v) => {
|
||||
v.has = this.checkBool(v, 'has')
|
||||
v.can = this.checkBool(v, 'can')
|
||||
v.callback = this.cleanCallback(v)
|
||||
return v
|
||||
})
|
||||
return actions
|
||||
}
|
||||
return []
|
||||
},
|
||||
cleanMoreActions() {
|
||||
if (this.col.actions.extraActions instanceof Array) {
|
||||
const copy = _.cloneDeep(this.col.actions.extraActions)
|
||||
let actions = [...copy]
|
||||
actions = actions.map((v) => {
|
||||
v.has = this.checkBool(v, 'has')
|
||||
v.can = this.checkBool(v, 'can')
|
||||
v.callback = this.cleanCallback(v)
|
||||
return v
|
||||
})
|
||||
return actions
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.col)
|
||||
},
|
||||
methods: {
|
||||
checkBool(item, attr, defaults) {
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
let ok = item[attr]
|
||||
if (ok && typeof ok === 'function') {
|
||||
ok = ok(this.row, this.cellValue)
|
||||
} else if (ok == null) {
|
||||
ok = defaults === undefined ? true : defaults
|
||||
}
|
||||
return ok
|
||||
},
|
||||
cleanCallback(item) {
|
||||
const callback = item.callback
|
||||
const attrs = {
|
||||
reload: this.reload,
|
||||
row: this.row,
|
||||
col: this.col,
|
||||
cellValue: this.cellValue,
|
||||
tableData: this.tableData
|
||||
}
|
||||
return () => { return callback.bind(this)(attrs) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<ActionsGroup v-loading="loadStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionsGroup from '@/components/ActionsGroup/index'
|
||||
import BaseFormatter from './base'
|
||||
|
||||
export default {
|
||||
name: 'LoadingActionsFormatter',
|
||||
components: { ActionsGroup },
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {
|
||||
hasUpdate: true, // can set function(row, value)
|
||||
canUpdate: true, // can set function(row, value)
|
||||
hasDelete: true, // can set function(row, value)
|
||||
canDelete: true,
|
||||
updateRoute: this.$route.name.replace('List', 'Update'),
|
||||
extraActions: [] // format see defaultActions
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const colActions = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
const defaultActions = [
|
||||
{
|
||||
name: 'update',
|
||||
title: this.$t('common.Update'),
|
||||
type: 'primary',
|
||||
has: colActions.hasUpdate,
|
||||
can: colActions.canUpdate,
|
||||
callback: colActions.onUpdate
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
title: this.$t('common.Delete'),
|
||||
type: 'danger',
|
||||
has: colActions.hasDelete,
|
||||
can: colActions.canDelete,
|
||||
callback: colActions.onDelete
|
||||
}
|
||||
]
|
||||
return {
|
||||
colActions: colActions,
|
||||
defaultActions: defaultActions,
|
||||
extraActions: colActions.extraActions
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cleanedActions() {
|
||||
let actions = [...this.defaultActions, ...this.extraActions]
|
||||
actions = _.cloneDeep(actions)
|
||||
actions = actions.map((v) => {
|
||||
v.has = this.cleanBoolean(v, 'has')
|
||||
v.can = this.cleanBoolean(v, 'can')
|
||||
v.fa = this.cleanFa(v, 'fa')
|
||||
v.callback = this.cleanCallback(v)
|
||||
return v
|
||||
})
|
||||
actions = actions.filter((v) => v.has)
|
||||
return actions
|
||||
},
|
||||
actions() {
|
||||
if (this.cleanedActions.length <= 2) {
|
||||
return this.cleanedActions
|
||||
}
|
||||
return this.cleanedActions.slice(0, 1)
|
||||
},
|
||||
moreActions() {
|
||||
if (this.cleanedActions.length <= 2) {
|
||||
return []
|
||||
}
|
||||
return this.cleanedActions.slice(1, this.cleanedActions.length)
|
||||
},
|
||||
loadStatus() {
|
||||
return this.col.formatterArgs.loading
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cleanBoolean(item, attr) {
|
||||
const ok = item[attr]
|
||||
if (typeof ok !== 'function') {
|
||||
return ok === undefined ? true : ok
|
||||
}
|
||||
return ok(this.row, this.cellValue)
|
||||
},
|
||||
cleanCallback(item) {
|
||||
const callback = item.callback
|
||||
const attrs = {
|
||||
reload: this.reload,
|
||||
row: this.row,
|
||||
col: this.col,
|
||||
cellValue: this.cellValue,
|
||||
tableData: this.tableData
|
||||
}
|
||||
return () => { return callback.bind(this)(attrs) }
|
||||
},
|
||||
cleanFa(item, attr) {
|
||||
const ok = item[attr]
|
||||
if (typeof ok !== 'function') {
|
||||
return ok === undefined ? false : ok
|
||||
}
|
||||
return ok(this.row, this.cellValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="headerActions" :selected-rows="selectedRows" :reload-table="reloadTable" />
|
||||
<IBox class="table-content">
|
||||
<AutoDataTable ref="dataTable" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
|
||||
<AutoDataTable ref="dataTable" :filter-table="filter" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
|
||||
</IBox>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ import IBox from '../IBox'
|
||||
import TableAction from './TableAction'
|
||||
import Emitter from '@/mixins/emitter'
|
||||
import deepmerge from 'deepmerge'
|
||||
|
||||
export default {
|
||||
name: 'ListTable',
|
||||
components: {
|
||||
@@ -46,6 +47,7 @@ export default {
|
||||
},
|
||||
iTableConfig() {
|
||||
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
|
||||
this.$log.debug('Header actions', this.headerActions)
|
||||
this.$log.debug('ListTable: iTableConfig change', config)
|
||||
return config
|
||||
}
|
||||
@@ -56,10 +58,14 @@ export default {
|
||||
this.$log.debug('ListTable: found extraQuery change')
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
tableColConfig: {
|
||||
handler() {
|
||||
this.$log.debug('ListTable: found colConfig change')
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
handleSelectionChange(val) {
|
||||
this.selectedRows = val
|
||||
@@ -68,8 +74,13 @@ export default {
|
||||
this.dataTable.getList()
|
||||
},
|
||||
search(attrs) {
|
||||
this.$emit('TagSearch', attrs)
|
||||
return this.dataTable.search(attrs, true)
|
||||
},
|
||||
filter(attrs) {
|
||||
this.$emit('TagFilter', attrs)
|
||||
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
|
||||
},
|
||||
handleDateChange(attrs) {
|
||||
this.$set(this.extraQuery, 'date_from', attrs[0].toISOString())
|
||||
this.$set(this.extraQuery, 'date_to', attrs[1].toISOString())
|
||||
@@ -81,6 +92,7 @@ export default {
|
||||
date_from: attrs[0].toISOString(),
|
||||
date_to: attrs[1].toISOString()
|
||||
}
|
||||
this.$emit('TagDateChange', attrs)
|
||||
return this.dataTable.searchDate(query)
|
||||
},
|
||||
toggleRowSelection(row, isSelected) {
|
||||
@@ -92,28 +104,28 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.table-content {
|
||||
margin-top: 10px;
|
||||
.table-content {
|
||||
margin-top: 10px;
|
||||
|
||||
& >>> .el-card__body {
|
||||
padding: 0;
|
||||
}
|
||||
& >>> .el-table__header thead > tr > th {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
|
||||
/*background: white;*/
|
||||
/*}*/
|
||||
|
||||
/*& >>> .el-table th, .el-table tr {*/
|
||||
/*background-color: red;*/
|
||||
/*!*background-color: #FAFAFA;*!*/
|
||||
/*}*/
|
||||
& >>> .el-card__body {
|
||||
padding: 0;
|
||||
}
|
||||
& >>> .el-table__header thead > tr > th {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
//修改颜色
|
||||
// .el-button--text{
|
||||
// color: #409EFF;
|
||||
// }
|
||||
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
|
||||
/*background: white;*/
|
||||
/*}*/
|
||||
|
||||
/*& >>> .el-table th, .el-table tr {*/
|
||||
/*background-color: red;*/
|
||||
/*!*background-color: #FAFAFA;*!*/
|
||||
/*}*/
|
||||
}
|
||||
|
||||
//修改颜色
|
||||
// .el-button--text{
|
||||
// color: #409EFF;
|
||||
// }
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Switcher from '../Swicher'
|
||||
import Switcher from '../FormFields/Swicher'
|
||||
export default {
|
||||
name: 'ActionItem',
|
||||
components: {
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<template>
|
||||
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
|
||||
<table style="width: 100%">
|
||||
<table style="width: 100%;table-layout:fixed;" class="CardTable">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<Select2 ref="select2" v-model="select2.value" v-bind="select2" />
|
||||
<Select2 ref="select2" v-model="select2.value" :disabled="iDisabled" v-bind="select2" />
|
||||
</td>
|
||||
</tr>
|
||||
<slot />
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<el-button :type="type" size="small" :loading="submitLoading" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
<el-button :type="type" size="small" :loading="submitLoading" :disabled="iDisabled" @click="addObjects">
|
||||
{{ $t('common.Add') }}
|
||||
</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="showHasObjects">
|
||||
<tr v-for="obj of iHasObjects" :key="obj.value" style="width: 100%" class="item">
|
||||
<td><b>{{ obj.label }}</b></td>
|
||||
<tr v-for="obj of iHasObjects" :key="obj.value" class="item">
|
||||
<td style="width: 100%;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
|
||||
<el-tooltip style="margin: 4px;" effect="dark" :content="obj.label" placement="left">
|
||||
<b>{{ obj.label }}</b>
|
||||
</el-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<el-button size="mini" type="danger" style="float: right" @click="removeObject(obj)">
|
||||
<el-button size="mini" :disabled="iDisabled" type="danger" style="float: right" @click="removeObject(obj)">
|
||||
<i class="fa fa-minus" />
|
||||
</el-button>
|
||||
</td>
|
||||
@@ -24,7 +30,7 @@
|
||||
</template>
|
||||
<tr v-if="params.hasMore && showHasMore" class="item">
|
||||
<td colspan="2">
|
||||
<el-button :type="type" size="small" style="width: 100%" @click="loadMore">
|
||||
<el-button :type="type" :disabled="iDisabled" size="small" style="width: 100%" @click="loadMore">
|
||||
<i class="fa fa-arrow-down" />
|
||||
{{ $t('common.More') }}
|
||||
</el-button>
|
||||
@@ -35,9 +41,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '../Select2'
|
||||
import Select2 from '../FormFields/Select2'
|
||||
import IBox from '../IBox'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
name: 'RelationCard',
|
||||
components: {
|
||||
@@ -83,6 +90,10 @@ export default {
|
||||
type: [Array, Number, String],
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: null
|
||||
},
|
||||
showHasMore: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@@ -134,11 +145,13 @@ export default {
|
||||
ajax: this.objectsAjax,
|
||||
options: this.objects,
|
||||
value: this.value,
|
||||
disabled: this.disabled,
|
||||
disabledValues: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentOrgIsRoot']),
|
||||
iAjax() {
|
||||
return this.$refs.select2.iAjax
|
||||
},
|
||||
@@ -147,6 +160,12 @@ export default {
|
||||
},
|
||||
hasObjectLeftLength() {
|
||||
return this.totalHasObjectsLength - this.iHasObjects.length
|
||||
},
|
||||
iDisabled() {
|
||||
if (this.disabled !== null) {
|
||||
return this.disabled
|
||||
}
|
||||
return this.currentOrgIsRoot
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<ActionsGroup :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
|
||||
<ActionsGroup v-loading="loadingStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionsGroup from '@/components/ActionsGroup/index'
|
||||
import ActionsGroup from '@/components/ActionsGroup'
|
||||
import BaseFormatter from './base'
|
||||
|
||||
const defaultPerformDelete = function({ row, col }) {
|
||||
@@ -24,6 +24,19 @@ const defaultUpdateCallback = function({ row, col }) {
|
||||
this.$router.push(route)
|
||||
}
|
||||
|
||||
const defaultCloneCallback = function({ row, col }) {
|
||||
const id = row.id
|
||||
let route = { query: { clone_from: id }}
|
||||
const cloneRoute = this.colActions.cloneRoute
|
||||
|
||||
if (typeof cloneRoute === 'object') {
|
||||
route = Object.assign(route, cloneRoute)
|
||||
} else {
|
||||
route.name = cloneRoute
|
||||
}
|
||||
this.$router.push(route)
|
||||
}
|
||||
|
||||
const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
|
||||
let msg = this.$t('common.deleteWarningMsg')
|
||||
const name = row.name || row.hostname
|
||||
@@ -68,13 +81,21 @@ export default {
|
||||
default: function() {
|
||||
return {
|
||||
hasUpdate: true, // can set function(row, value)
|
||||
canUpdate: true, // can set function(row, value)
|
||||
canUpdate: () => {
|
||||
return !this.$store.getters.currentOrgIsRoot
|
||||
}, // can set function(row, value)
|
||||
hasDelete: true, // can set function(row, value)
|
||||
canDelete: true,
|
||||
hasClone: true,
|
||||
canClone: () => {
|
||||
return !this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
updateRoute: this.$route.name.replace('List', 'Update'),
|
||||
cloneRoute: this.$route.name.replace('List', 'Create'),
|
||||
performDelete: defaultPerformDelete,
|
||||
onUpdate: defaultUpdateCallback,
|
||||
onDelete: defaultDeleteCallback,
|
||||
onClone: defaultCloneCallback,
|
||||
extraActions: [] // format see defaultActions
|
||||
}
|
||||
}
|
||||
@@ -89,7 +110,8 @@ export default {
|
||||
type: 'primary',
|
||||
has: colActions.hasUpdate,
|
||||
can: colActions.canUpdate,
|
||||
callback: colActions.onUpdate
|
||||
callback: colActions.onUpdate,
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
@@ -97,7 +119,17 @@ export default {
|
||||
type: 'danger',
|
||||
has: colActions.hasDelete,
|
||||
can: colActions.canDelete,
|
||||
callback: colActions.onDelete
|
||||
callback: colActions.onDelete,
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
name: 'clone',
|
||||
title: this.$t('common.Clone'),
|
||||
type: 'info',
|
||||
has: colActions.hasClone,
|
||||
can: colActions.canClone,
|
||||
callback: colActions.onClone,
|
||||
order: 30
|
||||
}
|
||||
]
|
||||
return {
|
||||
@@ -112,12 +144,16 @@ export default {
|
||||
let actions = [...this.defaultActions, ...this.extraActions]
|
||||
actions = _.cloneDeep(actions)
|
||||
actions = actions.map((v) => {
|
||||
v.has = this.cleanBoolean(v, 'has')
|
||||
v.can = this.cleanBoolean(v, 'can')
|
||||
v.callback = this.cleanCallback(v)
|
||||
v.has = this.cleanBoolean(v, 'has', true)
|
||||
v.can = this.cleanBoolean(v, 'can', true)
|
||||
v.callback = this.cleanCallback(v, 'callback')
|
||||
v.fa = this.cleanValue(v, 'fa')
|
||||
v.order = v.order || 100
|
||||
v.tip = this.cleanValue(v, 'tip')
|
||||
return v
|
||||
})
|
||||
actions = actions.filter((v) => v.has)
|
||||
actions.sort((a, b) => a.order - b.order)
|
||||
return actions
|
||||
},
|
||||
actions() {
|
||||
@@ -131,18 +167,21 @@ export default {
|
||||
return []
|
||||
}
|
||||
return this.cleanedActions.slice(1, this.cleanedActions.length)
|
||||
},
|
||||
loadingStatus() {
|
||||
return this.col.formatterArgs.loading
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cleanBoolean(item, attr) {
|
||||
cleanBoolean(item, attr, defaults) {
|
||||
const ok = item[attr]
|
||||
if (typeof ok !== 'function') {
|
||||
return ok === undefined ? true : ok
|
||||
return ok === undefined ? defaults : ok
|
||||
}
|
||||
return ok(this.row, this.cellValue)
|
||||
return this.cleanValue(item, attr)
|
||||
},
|
||||
cleanCallback(item) {
|
||||
const callback = item.callback
|
||||
cleanCallback(item, attr) {
|
||||
const callback = item[attr]
|
||||
const attrs = {
|
||||
reload: this.reload,
|
||||
row: this.row,
|
||||
@@ -151,6 +190,20 @@ export default {
|
||||
tableData: this.tableData
|
||||
}
|
||||
return () => { return callback.bind(this)(attrs) }
|
||||
},
|
||||
cleanValue(item, attr) {
|
||||
const value = item[attr]
|
||||
if (!value || typeof value !== 'function') {
|
||||
return value
|
||||
}
|
||||
const attrs = {
|
||||
reload: this.reload,
|
||||
row: this.row,
|
||||
col: this.col,
|
||||
cellValue: this.cellValue,
|
||||
tableData: this.tableData
|
||||
}
|
||||
return value(attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/components/TableFormatters/ArrayFormatter.vue
Normal file
15
src/components/TableFormatters/ArrayFormatter.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<span>{{ cellValue.toString() }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base'
|
||||
export default {
|
||||
name: 'ArrayFormatter',
|
||||
extends: BaseFormatter
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
42
src/components/TableFormatters/BooleanFormatter.vue
Normal file
42
src/components/TableFormatters/BooleanFormatter.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<i :class="'fa ' + iconClass" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base'
|
||||
export default {
|
||||
name: 'ChoicesFormatter',
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
iconChoices: {
|
||||
true: 'fa-check text-primary',
|
||||
false: 'fa-times text-danger'
|
||||
},
|
||||
typeChange(val) {
|
||||
return !!val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconClass() {
|
||||
const key = this.formatterArgs.typeChange(this.cellValue)
|
||||
return this.formatterArgs.iconChoices[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -28,6 +28,9 @@ export default {
|
||||
},
|
||||
hasTips: false,
|
||||
tipStatus(val, vm) {
|
||||
if (!val) {
|
||||
return vm.$t('assets.Unknown')
|
||||
}
|
||||
if (val.status === 0) {
|
||||
return vm.$t('assets.Unreachable')
|
||||
} else if (val.status === 1) {
|
||||
@@ -55,6 +58,9 @@ export default {
|
||||
return this.formatterArgs.tipStatus(this.cellValue, vm)
|
||||
},
|
||||
tipTime() {
|
||||
if (!this.cellValue) {
|
||||
return ''
|
||||
}
|
||||
return toSafeLocalDateStr(this.cellValue.datetime)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-button ref="deleteButton" size="mini" type="danger" :disabled="canDelete" @click="onDelete(col, row, cellValue, reload)">
|
||||
<el-button ref="deleteButton" size="mini" type="danger" :disabled="iDisabled" @click="onDelete(col, row, cellValue, reload)">
|
||||
<i class="fa fa-minus" />
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -11,8 +11,9 @@ export default {
|
||||
name: 'DeleteActionFormatter',
|
||||
extends: BaseFormatter,
|
||||
computed: {
|
||||
canDelete() {
|
||||
return this.iCanDelete()
|
||||
iDisabled() {
|
||||
// 禁用
|
||||
return (this.disabled() || this.$store.getters.currentOrgIsRoot)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -22,7 +23,7 @@ export default {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
reload()
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
|
||||
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
|
||||
})
|
||||
},
|
||||
onDelete(col, row, cellValue, reload) {
|
||||
@@ -32,7 +33,7 @@ export default {
|
||||
this.defaultOnDelete(col, row, cellValue, reload)
|
||||
}
|
||||
},
|
||||
iCanDelete() {
|
||||
disabled() {
|
||||
if (this.col.objects === 'all') {
|
||||
return false
|
||||
}
|
||||
84
src/components/TableFormatters/EditableInputFormatter.vue
Normal file
84
src/components/TableFormatters/EditableInputFormatter.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div style="width: 100%;min-height: 20px" @click.stop="editCell">
|
||||
<el-input
|
||||
v-if="inEditMode"
|
||||
v-model="value"
|
||||
size="mini"
|
||||
class="editInput"
|
||||
@keyup.enter.native="onInputEnter"
|
||||
@blur="onInputEnter"
|
||||
/>
|
||||
<template v-else>
|
||||
<span>{{ cellValue }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base'
|
||||
|
||||
export default {
|
||||
name: 'EditableInputFormatter',
|
||||
components: {
|
||||
},
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
trigger: 'click',
|
||||
onEnter: ({ row, col, oldValue, newValue }) => {
|
||||
const prop = col.prop
|
||||
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
|
||||
this.$set(row, prop, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const valueIsString = typeof this.cellValue === 'string'
|
||||
const jsonValue = this.cellValue ? JSON.stringify(this.cellValue) : ''
|
||||
return {
|
||||
inEditMode: false,
|
||||
value: valueIsString ? this.cellValue || '' : jsonValue,
|
||||
valueIsString: valueIsString,
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editCell() {
|
||||
this.inEditMode = true
|
||||
},
|
||||
onInputEnter() {
|
||||
let validValue = this.value
|
||||
try {
|
||||
validValue = JSON.parse(validValue)
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
this.formatterArgs.onEnter({
|
||||
row: this.row, col: this.col,
|
||||
oldValue: this.cellValue,
|
||||
newValue: validValue
|
||||
})
|
||||
this.inEditMode = false
|
||||
},
|
||||
cancelEdit() {
|
||||
this.inEditMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editInput >>> .el-input__inner {
|
||||
padding: 2px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.editInput {
|
||||
padding: -6px;
|
||||
}
|
||||
</style>
|
||||
68
src/components/TableFormatters/StatusFormatter.vue
Normal file
68
src/components/TableFormatters/StatusFormatter.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
|
||||
<div slot="content">
|
||||
<template v-if="tipsIsArray">
|
||||
<div v-for="tip of tips" :key="tip">
|
||||
<span>{{ tip }}</span>
|
||||
<br>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ tips }}
|
||||
</span>
|
||||
</div>
|
||||
<i :class="'fa ' + iconClass" />
|
||||
</el-tooltip>
|
||||
<i v-else :class="'fa ' + iconClass" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base'
|
||||
export default {
|
||||
name: 'StatusFormatter',
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
iconChoices: {
|
||||
true: 'fa-check text-primary',
|
||||
false: 'fa-times text-danger'
|
||||
},
|
||||
getChoicesKey(val) {
|
||||
return !!val
|
||||
},
|
||||
getTip(val, col) {
|
||||
},
|
||||
hasTips: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconClass() {
|
||||
const key = this.formatterArgs.getChoicesKey(this.cellValue)
|
||||
return this.formatterArgs.iconChoices[key] + ' ' + key + 'Status'
|
||||
},
|
||||
tips() {
|
||||
const vm = this
|
||||
return this.formatterArgs.getTip(this.cellValue, vm)
|
||||
},
|
||||
tipsIsArray() {
|
||||
return Array.isArray(this.tips)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,39 +1,45 @@
|
||||
import DetailFormatter from './DetailFormatter'
|
||||
import ArrayFormatter from './ArrayFormatter'
|
||||
import DisplayFormatter from './DisplayFormatter'
|
||||
import BooleanFormatter from './ChoicesFormatter'
|
||||
import BooleanFormatter from './BooleanFormatter'
|
||||
import ChoicesFormatter from './ChoicesFormatter'
|
||||
import ActionsFormatter from './ActionsFormatter'
|
||||
import CustomActionsFormatter from './CustomActionsFormatter'
|
||||
import DeleteActionFormatter from './DeleteActionFormatter'
|
||||
import DateFormatter from './DateFormatter'
|
||||
import SystemUserFormatter from './GrantedSystemUsersShowFormatter'
|
||||
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
|
||||
import ShowKeyFormatter from '@/components/TableFormatters/ShowKeyFormatter'
|
||||
import DialogDetailFormatter from './DialogDetailFormatter'
|
||||
import LoadingActionsFormatter from './LoadingActionsFormatter'
|
||||
import EditableInputFormatter from './EditableInputFormatter'
|
||||
import StatusFormatter from './StatusFormatter'
|
||||
|
||||
export default {
|
||||
DetailFormatter,
|
||||
DisplayFormatter,
|
||||
BooleanFormatter,
|
||||
ChoicesFormatter,
|
||||
ActionsFormatter,
|
||||
CustomActionsFormatter,
|
||||
DeleteActionFormatter,
|
||||
DateFormatter,
|
||||
SystemUserFormatter,
|
||||
ShowKeyFormatter,
|
||||
DialogDetailFormatter,
|
||||
LoadingActionsFormatter
|
||||
ArrayFormatter,
|
||||
EditableInputFormatter,
|
||||
StatusFormatter
|
||||
}
|
||||
|
||||
export {
|
||||
DetailFormatter,
|
||||
DisplayFormatter,
|
||||
BooleanFormatter,
|
||||
ChoicesFormatter,
|
||||
ActionsFormatter,
|
||||
CustomActionsFormatter,
|
||||
DeleteActionFormatter,
|
||||
DateFormatter,
|
||||
SystemUserFormatter,
|
||||
ShowKeyFormatter,
|
||||
DialogDetailFormatter,
|
||||
LoadingActionsFormatter
|
||||
ArrayFormatter,
|
||||
EditableInputFormatter,
|
||||
StatusFormatter
|
||||
}
|
||||
@@ -5,8 +5,10 @@
|
||||
<component
|
||||
:is="component"
|
||||
ref="AutoDataZTree"
|
||||
:key="componentTreeKey"
|
||||
:setting="treeSetting"
|
||||
class="auto-data-ztree"
|
||||
v-on="$listeners"
|
||||
@urlChange="handleUrlChange"
|
||||
>
|
||||
<div slot="rMenu" slot-scope="{data}">
|
||||
@@ -22,7 +24,7 @@
|
||||
</div>
|
||||
<div class="transition-box" style="width: calc(100% - 17px);">
|
||||
<slot name="table">
|
||||
<ListTable ref="ListTable" :key="componentKey" :table-config="iTableConfig" :header-actions="headerActions" />
|
||||
<ListTable ref="ListTable" :key="componentKey" :table-config="iTableConfig" :header-actions="headerActions" v-on="$listeners" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,10 +65,16 @@ export default {
|
||||
return {
|
||||
iTableConfig: this.tableConfig,
|
||||
iShowTree: this.showTree,
|
||||
componentKey: 0
|
||||
componentKey: 0,
|
||||
componentTreeKey: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
treeConfig: {
|
||||
handler(val) {
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleUrlChange(_url) {
|
||||
@@ -77,11 +85,20 @@ export default {
|
||||
forceRerender() {
|
||||
this.componentKey += 1
|
||||
},
|
||||
forceRerenderTree() {
|
||||
this.componentTreeKey += 1
|
||||
},
|
||||
hideRMenu() {
|
||||
this.$refs.AutoDataZTree.hideRMenu()
|
||||
},
|
||||
getSelectedNodes: function() {
|
||||
return this.$refs.AutoDataZTree.getSelectedNodes()
|
||||
},
|
||||
getNodes: function() {
|
||||
return this.$refs.AutoDataZTree.getNodes()
|
||||
},
|
||||
selectNode: function(node) {
|
||||
return this.$refs.AutoDataZTree.selectNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,15 @@ export { default as FormGroupHeader } from './FormGroupHeader'
|
||||
export { default as Hamburger } from './Hamburger'
|
||||
export { default as ListTable } from './ListTable'
|
||||
export { default as RelationCard } from './RelationCard'
|
||||
export { default as Select2 } from './Select2'
|
||||
export { default as Select2 } from './FormFields/Select2'
|
||||
export { default as UploadKey } from './FormFields/UploadKey.vue'
|
||||
export { default as AssetSelect } from './AssetSelect'
|
||||
export { default as SvgIcon } from './SvgIcon'
|
||||
export { default as TreeTable } from './TreeTable'
|
||||
export { default as IBox } from './IBox'
|
||||
export { default as QuickActions } from './QuickActions'
|
||||
export { default as Switcher } from './Swicher'
|
||||
export { default as Switcher } from './FormFields/Swicher'
|
||||
export { default as SummaryCard } from './SummaryCard'
|
||||
export { default as UploadField } from './UploadField'
|
||||
export { default as UploadField } from './FormFields/UploadField'
|
||||
export { default as AssetUserTable } from './AssetUserTable'
|
||||
export { default as AssetRelationCard } from './AssetRelationCard'
|
||||
|
||||
@@ -1,9 +1,56 @@
|
||||
{
|
||||
"": "",
|
||||
"accounts": {
|
||||
"PleaseClickLeftAssetToViewAssetAccount": "资产账号列表,点击左侧资产进行查看",
|
||||
"PleaseClickLeftApplicationToViewApplicationAccount": "应用账号列表,点击左侧应用进行查看",
|
||||
"PleaseClickLeftAssetToViewGatheredUser": "收集用户列表,点击左侧资产进行查看"
|
||||
},
|
||||
"acl": {
|
||||
"name": "名称",
|
||||
"username": "用户名",
|
||||
"ip_group": "IP 组",
|
||||
"action": "动作",
|
||||
"priority": "优先级",
|
||||
"date_created": "创建时间",
|
||||
"created_by": "创建者",
|
||||
"asset": "资产信息",
|
||||
"users":"用户信息",
|
||||
"system_user": "系统用户",
|
||||
"username_group":"用户名",
|
||||
"hostname_group":"资产名",
|
||||
"asset_ip_group": "资产IP",
|
||||
"system_users_name_group": "系统用户名称",
|
||||
"system_users_protocol_group": "系统用户协议",
|
||||
"system_users_username_group": "系统用户名",
|
||||
"apply_login_asset": "申请登录资产",
|
||||
"apply_login_system_user": "申请登录系统用户",
|
||||
"apply_login_user": "申请登录用户",
|
||||
"RuleDetail": "规则详情"
|
||||
},
|
||||
"applications": {
|
||||
"": "",
|
||||
"RemoteApp": "远程应用",
|
||||
"Database": "数据库",
|
||||
"Cloud": "云",
|
||||
"applicationsType": {
|
||||
"chrome": "Chrome",
|
||||
"mysql_workbench": "MySQL Workbench",
|
||||
"vmware_client":"Vmware Client",
|
||||
"custom":"Custom",
|
||||
"mysql": "MySQL",
|
||||
"oracle": "Oracle",
|
||||
"postgresql": "PostgreSQL",
|
||||
"mariadb": "MariaDB",
|
||||
"k8s": "Kubernetes"
|
||||
},
|
||||
"applicationsCategory": {
|
||||
"remote_app": "远程应用",
|
||||
"db": "数据库应用",
|
||||
"cloud": "云应用"
|
||||
},
|
||||
"appPath": "应用路径",
|
||||
"appType": "应用类型",
|
||||
"appName": "应用名称",
|
||||
"asset": "资产",
|
||||
"database": "数据库",
|
||||
"host": "主机",
|
||||
@@ -32,7 +79,8 @@
|
||||
"Custom": "自定义",
|
||||
"cluster": "集群",
|
||||
"kubernetes":"Kubernetes",
|
||||
"clusterHelpTextMessage": "例如:https://172.16.8.8:8443"
|
||||
"clusterHelpTextMessage": "例如:https://172.16.8.8:8443",
|
||||
"DBInfo": "数据库信息"
|
||||
},
|
||||
"assets": {
|
||||
"Action": "动作",
|
||||
@@ -41,8 +89,10 @@
|
||||
"AdminUserDetail": "管理用户详情",
|
||||
"AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n",
|
||||
"Asset": "资产",
|
||||
"HardwareInfo": "硬件信息",
|
||||
"AssetDetail": "资产详情",
|
||||
"AssetList": "资产列表",
|
||||
"ReplaceNodeAssetsAdminUser":"替换节点资产的管理员",
|
||||
"AssetListHelpMessage": "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产\n",
|
||||
"TestGatewayTestConnection":"测试连接网关",
|
||||
"TestGatewayHelpMessage": "如果使用了nat端口映射,请设置为ssh真实监听的端口",
|
||||
@@ -51,6 +101,7 @@
|
||||
"AssetUserList": "资产用户列表",
|
||||
"Assets": "资产",
|
||||
"Auth": "认证",
|
||||
"AccountList": "账号列表",
|
||||
"AutoGenerateKey": "自动生成密钥",
|
||||
"AutoPush": "自动推送",
|
||||
"BasePlatform": "基础平台",
|
||||
@@ -70,6 +121,8 @@
|
||||
"DateUpdated": "更新日期",
|
||||
"DeactiveSelected": "禁用所选",
|
||||
"Disk": "硬盘",
|
||||
"AdDomain": "AD域名",
|
||||
"AdDomainHelpText": "提供给域用户登录的AD域名",
|
||||
"Domain": "网域",
|
||||
"DomainDetail": "网域详情",
|
||||
"DomainHelpMessage": "网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。JMS => 网域网关 => 目标资产",
|
||||
@@ -94,6 +147,7 @@
|
||||
"OnlyLatestVersion": "仅最新版本",
|
||||
"Os": "操作系统",
|
||||
"Other": "其它",
|
||||
"Hardware": "硬件信息",
|
||||
"Password": "密码",
|
||||
"PasswordWithoutSpecialCharHelpText": "不能包含特殊字符",
|
||||
"Pending": "等待",
|
||||
@@ -113,6 +167,7 @@
|
||||
"RefreshHardware": "更新硬件信息",
|
||||
"RemoteAppListHelpMessage": "使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 RemoteApp 应用 <b><a href='https://github.com/jumpserver/Jmservisor/releases'>下载应用加载器</a></b>",
|
||||
"RemoteApps": "远程应用",
|
||||
"Applications": "应用",
|
||||
"RemoteType": "应用类型",
|
||||
"RemoveFromCurrentNode": "从节点移除",
|
||||
"ReplaceNodeAssetsAdminUserWithThis": "替换资产的管理员",
|
||||
@@ -129,7 +184,7 @@
|
||||
"TestAssetsConnective": "测试资产可连接性",
|
||||
"TestConnection": "测试连接",
|
||||
"Type": "类型",
|
||||
"UnselectedAssets": "未选择资产",
|
||||
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
|
||||
"UnselectedNodes": "未选择节点",
|
||||
"UpdateAssetUserToken": "更新资产用户认证信息",
|
||||
"Username": "用户名",
|
||||
@@ -139,39 +194,57 @@
|
||||
"Version": "版本",
|
||||
"command_filter_list": "命令过滤器列表",
|
||||
"date_joined": "创建日期",
|
||||
"sshKeyFingerprint": "SSH 指纹",
|
||||
"ip": "IP",
|
||||
"sshkey": "sshkey",
|
||||
"GroupsHelpMessage": "请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)",
|
||||
"HomeHelpMessage": "默认家目录 /home/系统用户名: /home/username",
|
||||
"Home": "家目录",
|
||||
"LinuxUserAffiliateGroup": "用户附属组",
|
||||
"ipDomain": "IP(域名)"
|
||||
|
||||
"ipDomain": "IP(域名)",
|
||||
"HostProtocol": "主机协议",
|
||||
"DatabaseProtocol": "数据库协议",
|
||||
"OtherProtocol": "其它协议",
|
||||
"PasswordOrToken": "密码 / 令牌"
|
||||
},
|
||||
"audits": {
|
||||
"Hosts": "主机",
|
||||
"RunUser": "运行用户",
|
||||
"User": "用户",
|
||||
"View": "查看"
|
||||
"Username": "用户名",
|
||||
"View": "查看",
|
||||
"SystemUserName": "系统用户名"
|
||||
},
|
||||
"auth": {
|
||||
"LoginRequiredMsg": "账号已退出,请重新登录",
|
||||
"ReLogin": "重新登录"
|
||||
},
|
||||
"common": {
|
||||
"ConnectWebSocketError": "连接 WebSocket 失败",
|
||||
"Action": "动作",
|
||||
"RequestTickets": "申请工单",
|
||||
"Actions": "操作",
|
||||
"CustomCol":"自定义列表字段",
|
||||
"Activate": "激活",
|
||||
"NeedSpecifiedFile": "需上传指定格式文件",
|
||||
"TestPortErrorMsg":"端口错误,请重新输入",
|
||||
"Active": "激活中",
|
||||
"actionsTips":"剪切板权限控制目前仅支持 RDP/VNC 协议的连接",
|
||||
"TableColSettingInfo": "请选择您想显示的列表详细信息。",
|
||||
"Add": "添加",
|
||||
"UpdateAssetDetail": "配置更多信息",
|
||||
"AddSuccessMsg": "添加成功",
|
||||
"Auth": "认证",
|
||||
"bind": "绑定",
|
||||
"unbind": "解绑",
|
||||
"PushSelected":"推送所选",
|
||||
"BadRequestErrorMsg": "请求错误,请检查填写内容",
|
||||
"BadRoleErrorMsg": "请求错误,无该操作权限",
|
||||
"BadConflictErrorMsg": "正在刷新中,请稍后再试",
|
||||
"Basic": "基本",
|
||||
"PleaseAgreeToTheTerms": "请同意条款",
|
||||
"BasicInfo": "基本信息",
|
||||
"ApplyInfo": "申请信息",
|
||||
"Cancel": "取消",
|
||||
"Close": "关闭",
|
||||
"Command filter": "命令过滤器",
|
||||
@@ -181,6 +254,7 @@
|
||||
"CreatedBy": "创建者",
|
||||
"CrontabHelpTips": "eg:每周日 03:05 执行 <5 3 * * 0> <br> 提示: 使用5位 Linux crontab 表达式 <分 时 日 月 星期> (<a href='https://tool.lu/crontab/' target='_blank'>在线工具</a>) <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行",
|
||||
"DateEnd": "结束日期",
|
||||
"Resource": "资源",
|
||||
"DateLast24Hours": "最近一天",
|
||||
"DateLast3Months": "最近三月",
|
||||
"DateLastMonth": "最近一月",
|
||||
@@ -194,10 +268,16 @@
|
||||
"EnterForSearch": "按回车进行搜索",
|
||||
"Export": "导出",
|
||||
"Import": "导入",
|
||||
"ContinueImport": "继续导入",
|
||||
"Continue": "继续",
|
||||
"Stop": "停止",
|
||||
"Finished": "完成",
|
||||
"Refresh": "刷新",
|
||||
"Info": "提示",
|
||||
"MFAConfirm": "MFA 认证",
|
||||
"MFARequireForSecurity": "为了安全请输入MFA",
|
||||
"PasswordConfirm": "密码认证",
|
||||
"PasswordRequireForSecurity": "为了安全请输入密码",
|
||||
"Members": "成员",
|
||||
"More": "更多",
|
||||
"Message": "消息",
|
||||
@@ -218,17 +298,20 @@
|
||||
"SelectFile": "选择文件",
|
||||
"Show": "显示",
|
||||
"Submit": "提交",
|
||||
"SaveAndAddAnother": "保存并继续添加",
|
||||
"Test": "测试",
|
||||
"TestSuccessMsg": "测试成功",
|
||||
"To": "至",
|
||||
"Update": "更新",
|
||||
"Upload": "上传",
|
||||
"Clone": "克隆",
|
||||
"Username": "用户名",
|
||||
"Validity": "有效",
|
||||
"Invalidity": "无效",
|
||||
"View": "查看",
|
||||
"Yes": "是",
|
||||
"action": "动作",
|
||||
"User": "用户",
|
||||
"activateSelected": "激活所选",
|
||||
"bulkDeleteErrorMsg": "批量删除失败: ",
|
||||
"bulkDeleteSuccessMsg": "批量删除成功",
|
||||
@@ -236,8 +319,10 @@
|
||||
"NeedAssetsAndSystemUserErrMsg": "请先选择授权的系统用户和资产",
|
||||
"bulkRemoveSuccessMsg": "批量移除成功",
|
||||
"createBy": "创建者",
|
||||
"cloneFrom": "克隆自",
|
||||
"createErrorMsg": "创建失败",
|
||||
"createSuccessMsg": "创建成功",
|
||||
"saveSuccessContinueMsg": "创建成功,更新内容后可以继续添加",
|
||||
"createdBy": "创建人",
|
||||
"dateCreated": "创建日期",
|
||||
"dateExpired": "失效日期",
|
||||
@@ -252,17 +337,28 @@
|
||||
"fieldRequiredError": "这个字段是必填项",
|
||||
"getErrorMsg": "获取失败",
|
||||
"MFAErrorMsg": "MFA错误,请检查",
|
||||
"Total": "总共",
|
||||
"Success": "成功",
|
||||
"Failed": "失败",
|
||||
"Pending": "等待",
|
||||
"Status": "状态",
|
||||
"InputEmailAddress": "请输入正确的邮箱地址",
|
||||
"Receivers": "接收人",
|
||||
"imExport": {
|
||||
"ExportAll": "导出所有",
|
||||
"ExportOnlyFiltered": "仅导出搜索结果",
|
||||
"ExportOnlySelectedItems": "仅导出选择项",
|
||||
"ExportRange": "导出范围",
|
||||
"createSuccessMsg": "导入创建成功,总共:{count}",
|
||||
"downloadImportTemplateMsg": "下载导入模板",
|
||||
"downloadImportTemplateMsg": "下载创建模板",
|
||||
"downloadUpdateTemplateMsg": "下载更新模板",
|
||||
"onlyCSVFilesTips": "仅支持csv文件导入",
|
||||
"updateSuccessMsg": "导入更新成功,总共:{count}"
|
||||
"updateSuccessMsg": "导入更新成功,总共:{count}",
|
||||
"uploadCsvLth10MHelpText": "只能上传 csv/xlsx, 且不超过 10M",
|
||||
"dragUploadFileInfo": "将文件拖到此处,或点击上传",
|
||||
"hasImportErrorItemMsg": "存在导入失败项,点击左侧 x 查看失败原因,点击表格编辑后,可以继续导入失败项"
|
||||
},
|
||||
"fileType": "文件类型",
|
||||
"isValid": "有效",
|
||||
"nav": {
|
||||
"APIKey": "API Key",
|
||||
@@ -297,7 +393,9 @@
|
||||
"NUMBER_REQUIRED": "须包含数字",
|
||||
"SPECIAL_CHAR_REQUIRED": "须包含特殊字符",
|
||||
"MIN_LENGTH_ERROR": "密码最小长度 {0} 位"
|
||||
}
|
||||
},
|
||||
"lastCannotBeDeleteMsg": "最后一项,不能被删除",
|
||||
"InvalidJson": "不是合法 JSON"
|
||||
},
|
||||
"dashboard": {
|
||||
"ActiveAsset": "近期被登录过",
|
||||
@@ -388,6 +486,11 @@
|
||||
"SystemUser": "系统用户",
|
||||
"User": "用户",
|
||||
"UserGroups": "用户组",
|
||||
"PermName":"授权名称",
|
||||
"DatabaseAppPermission": "数据库授权",
|
||||
"RemoteAppPermission": "远程应用授权",
|
||||
"addApplicationToThisPermission": "添加应用",
|
||||
"KubernetesAppPermission": "Kubernetes授权",
|
||||
"addAssetToThisPermission": "添加资产",
|
||||
"addDatabaseAppToThisPermission": "添加数据库应用",
|
||||
"addNodeToThisPermission": "添加节点",
|
||||
@@ -406,12 +509,15 @@
|
||||
"downloadFile": "下载文件",
|
||||
"hostName": "主机名",
|
||||
"isValid": "有效",
|
||||
"isEffective": "起作用的",
|
||||
"nodeCount": "节点数量",
|
||||
"refreshFail": "刷新失败",
|
||||
"refreshPermissionCache": "刷新授权缓存",
|
||||
"refreshSuccess": "刷新成功",
|
||||
"remoteApp": "远程应用",
|
||||
"remoteAppCount": "远程应用数量",
|
||||
"appsCount": "应用数量",
|
||||
"appsList":"应用列表",
|
||||
"DatabaseAppCount": "数据库应用数量",
|
||||
"KubernetesAppCount": "Kubernetes应用数量",
|
||||
"systemUserCount": "系统用户数量",
|
||||
@@ -426,7 +532,11 @@
|
||||
},
|
||||
"route": {
|
||||
"": "",
|
||||
"Accounts": "账号管理",
|
||||
"AssetAccount": "资产账号",
|
||||
"ApplicationAccount": "应用账号",
|
||||
"Ticket":"工单",
|
||||
"CommandConfirm": "命令复核",
|
||||
"AdminUserCreate": "创建管理用户",
|
||||
"AdminUserDetail": "管理用户详情",
|
||||
"AdminUserList": "管理用户",
|
||||
@@ -457,7 +567,7 @@
|
||||
"CreateCommandStorage": "创建命令存储",
|
||||
"CreateReplayStorage": "创建录像存储",
|
||||
"Dashboard": "仪表盘",
|
||||
"DatabaseApp": "数据库应用",
|
||||
"DatabaseApp": "数据库",
|
||||
"DatabaseAppCreate": "创建数据库应用",
|
||||
"DatabaseAppDetail": "数据库详情",
|
||||
"DatabaseAppPermission": "数据库授权",
|
||||
@@ -473,7 +583,16 @@
|
||||
"KubernetesAppPermissionDetail": "Kubernetes授权详情",
|
||||
"KubernetesAppPermissionUpdate": "更新Kubernetes授权规则",
|
||||
"KubernetesAppUpdate": "更新Kubernetes",
|
||||
|
||||
"Acl": "访问控制",
|
||||
"UserAclList": "用户登录",
|
||||
"UserAclCreate": "创建用户登录规则",
|
||||
"UserAclLists": "用户登录规则",
|
||||
"UserAclUpdate": "更新用户登录规则",
|
||||
"UserAclDetail": "用户登录规则详情",
|
||||
"AssetAclList": "登录资产",
|
||||
"AssetAclCreate": "创建资产登录规则",
|
||||
"AssetAclUpdate": "更新资产登录规则",
|
||||
"AssetAclDetail": "资产登录规则详情",
|
||||
"DomainCreate": "创建网域",
|
||||
"DomainDetail": "网域详情",
|
||||
"DomainList": "网域列表",
|
||||
@@ -503,6 +622,11 @@
|
||||
"RemoteAppPermissionCreate": "创建远程应用授权规则",
|
||||
"RemoteAppPermissionDetail": "远程应用授权详情",
|
||||
"RemoteAppPermissionUpdate": "更新远程应用授权规则",
|
||||
"ApplicationDetail": "应用详情",
|
||||
"ApplicationPermission": "应用授权",
|
||||
"ApplicationPermissionCreate": "创建应用授权规则",
|
||||
"ApplicationPermissionDetail": "应用授权详情",
|
||||
"ApplicationPermissionUpdate": "更新应用授权规则",
|
||||
"RemoteAppUpdate": "更新远程应用",
|
||||
"ReplayStorageUpdate": "更新录像存储",
|
||||
"SessionDetail": "会话详情",
|
||||
@@ -534,7 +658,9 @@
|
||||
"UserUpdate": "更新用户",
|
||||
"Users": "用户管理",
|
||||
"WebFTP": "文件管理",
|
||||
"WebTerminal": "Web终端"
|
||||
"WebTerminal": "Web终端",
|
||||
"Notifications": "通知",
|
||||
"SiteMessageList": "站内信"
|
||||
},
|
||||
"sessions": {
|
||||
"StorageConfiguration": "存储配置",
|
||||
@@ -571,6 +697,11 @@
|
||||
"name": "名称",
|
||||
"protocol": "协议",
|
||||
"region": "地域",
|
||||
"EsDisabled": "节点不可用, 请联系管理员",
|
||||
"sessionActiveCount": "在线会话数量",
|
||||
"systemCpuLoad": "CPU负载",
|
||||
"systemDiskUsedPercent": "硬盘使用率",
|
||||
"systemMemoryUsedPercent": "内存使用率",
|
||||
"remoteAddr": "远端地址",
|
||||
"replay": "回放",
|
||||
"replaySession": "回放会话",
|
||||
@@ -592,10 +723,11 @@
|
||||
"common": "普通"
|
||||
},
|
||||
"Monitor": "监控",
|
||||
"XRDPNotSupport": "RDP 客户端会话, 暂不支持监控",
|
||||
"sessionMonitor": "监控",
|
||||
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
|
||||
"helpText": {
|
||||
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。(eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com)",
|
||||
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。(eg: http://www.jumpserver.a.com:3000,http://www.jumpserver.b.com:3000)",
|
||||
"esIndex": "es提供默认index:jumpserver",
|
||||
"esDocType": "es默认文档类型:command",
|
||||
"s3Endpoint": "S3 格式: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China) 格式: http://s3.{REGION_NAME}.amazonaws.com.cn<br>如: http://s3.cn-north-1.amazonaws.com.cn",
|
||||
@@ -603,6 +735,7 @@
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"InsecureCommandNotifyToSubscription": "危险命令通知已升级到消息订阅中,支持更多通知方式",
|
||||
"ApiKeyList": "API Key 列表",
|
||||
"AssetCount": "资产数量",
|
||||
"Basic": "基本设置",
|
||||
@@ -621,6 +754,8 @@
|
||||
"PasswordCheckRule": "密码校验规则",
|
||||
"Security": "安全设置",
|
||||
"SecuritySetting": "安全设置",
|
||||
"SystemMessageSubscription": "系统消息订阅",
|
||||
"insecureCommandEmailUpdate": "点我设置",
|
||||
"SubscriptionID": "订阅授权ID",
|
||||
"Terminal": "终端设置",
|
||||
"all": "全部",
|
||||
@@ -651,6 +786,11 @@
|
||||
"emailTest": "测试连接",
|
||||
"emailUserSSL": "使用SSL",
|
||||
"emailUserTLS": "使用TLS",
|
||||
"InsecureCommandAlert": "危险命令告警",
|
||||
"SecurityInsecureCommandEmailReceiver": "告警接收邮件",
|
||||
"MailSend": "邮件发送",
|
||||
"LDAPServerInfo": "LDAP 服务器",
|
||||
"LDAPUser": "LDAP 用户",
|
||||
"helpText": {
|
||||
"ApiKeyList": "使用api key签名请求头,每个请求的头部是不一样的, 请查阅使用文档",
|
||||
"authLdapSearchFilter": "可能的选项是(cn或uid或sAMAccountName=%(user)s)",
|
||||
@@ -660,7 +800,7 @@
|
||||
"emailCustomUserCreatedHonorific": "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)",
|
||||
"emailCustomUserCreatedSignature": "提示: 邮件的署名 (例如: jumpserver)",
|
||||
"emailCustomUserCreatedSubject": "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)",
|
||||
"emailEmailFrom": "提示:发送邮件账号,默认使用SMTP账号作为发送账号",
|
||||
"emailEmailFrom": "",
|
||||
"emailHostPassword": "提示:一些邮件提供商需要输入的是Token",
|
||||
"emailRecipient": "提示:仅用来作为测试邮件收件人",
|
||||
"emailSubjectPrefix": "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、JumpServer",
|
||||
@@ -671,7 +811,8 @@
|
||||
"terminalHeartbeatInterval": "单位: 秒",
|
||||
"terminalSessionKeepDuration": "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不受影响)",
|
||||
"terminalTelnetRegex": "登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功",
|
||||
"userGuideUrl": "用户第一次登录,修改profile后重定向到地址"
|
||||
"userGuideUrl": "用户第一次登录,修改profile后重定向到地址",
|
||||
"SecurityInsecureCommandEmailReceiver": "多个邮箱时,以半角逗号','分隔"
|
||||
},
|
||||
"helpTip": {
|
||||
"emailUserSSL": "如果SMTP端口是465,通常需要启用SSL",
|
||||
@@ -682,7 +823,8 @@
|
||||
"securityPasswordNumber": "开启后,用户密码修改、重置必须包含数字字符",
|
||||
"securityPasswordSpecialChar": "开启后,用户密码修改、重置必须包含特殊字符",
|
||||
"securityPasswordUpperCase": "开启后,用户密码修改、重置必须包含大写字母",
|
||||
"securityServiceAccountRegistration": "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止"
|
||||
"securityServiceAccountRegistration": "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止",
|
||||
"SecurityInsecureCommand": "开启后,当资产上有危险命令执行时,会发送邮件告警通知"
|
||||
},
|
||||
"validatorMessage": {
|
||||
"EnsureThisValueIsGreaterThanOrEqualTo3": "请确保该值大于或者等于 3",
|
||||
@@ -727,17 +869,22 @@
|
||||
"refreshLdapCache":"刷新Ldap缓存,请稍后",
|
||||
"LicenseExpired": "许可证已经过期",
|
||||
"LicenseWillBe": "许可证即将在 ",
|
||||
"Expire": " 过期"
|
||||
},
|
||||
"settings": {
|
||||
"Expire": " 过期",
|
||||
"WeCom": "企业微信",
|
||||
"DingTalk": "钉钉",
|
||||
"dingTalkTest": "测试",
|
||||
"weComTest": "测试",
|
||||
"setting": "设置"
|
||||
},
|
||||
"tickets": {
|
||||
"PermissionName": "授权规则名称",
|
||||
"Accept": "同意",
|
||||
"AssignedMe": "待我审批",
|
||||
"Assignee": "处理人",
|
||||
"Assignees": "待处理人",
|
||||
"Close": "关闭",
|
||||
"OpenStatus":"开启",
|
||||
"CloseStatus":"关闭",
|
||||
"Comment": "备注",
|
||||
"MyTickets": "我发起的",
|
||||
"RequestPerm":"授权申请",
|
||||
@@ -746,10 +893,14 @@
|
||||
"reply": "回复",
|
||||
"status": "状态",
|
||||
"title": "标题",
|
||||
"action": "动作",
|
||||
"IPGroup": "IP 组",
|
||||
"type": "类型",
|
||||
"user": "用户",
|
||||
"Status": "状态",
|
||||
"Open": "待处理",
|
||||
"OrgName":"组织名称",
|
||||
"AssignedInfo":"审批信息",
|
||||
"OpenTicket": "创建工单",
|
||||
"HandleTicket": "处理工单",
|
||||
"FinishedTicket": "完成工单",
|
||||
@@ -758,6 +909,7 @@
|
||||
"Asset": "资产",
|
||||
"SystemUser": "系统用户",
|
||||
"RequestAssetPerm": "申请资产授权",
|
||||
"RequestApplicationPerm": "申请应用授权",
|
||||
"Applicant": "申请人",
|
||||
"Pending": "待处理",
|
||||
"Approved": "已同意",
|
||||
@@ -765,8 +917,15 @@
|
||||
"Closed": "已完成",
|
||||
"helpText": {
|
||||
"ips": "请输入逗号分割的IP地址组",
|
||||
"fuzzySearch": "支持模糊搜索"
|
||||
}
|
||||
"fuzzySearch": "支持模糊搜索",
|
||||
"application": "请输入逗号分割的应用名称组"
|
||||
},
|
||||
"ApplyRunUser": "申请运行的用户",
|
||||
"ApplyRunAsset": "申请运行的资产",
|
||||
"ApplyRunSystemUser": "申请运行的系统用户",
|
||||
"ApplyRunCommand": "申请运行的命令",
|
||||
"ApplyFromSession": "会话",
|
||||
"ApplyFromCMDFilterRule": "命令过滤规则"
|
||||
},
|
||||
"tree": {
|
||||
"AddAssetToNode": "添加资产到节点",
|
||||
@@ -776,11 +935,13 @@
|
||||
"RenameNode": "重命名节点",
|
||||
"ShowAssetAllChildrenNode": "显示所有子节点资产",
|
||||
"ShowAssetOnlyCurrentNode": "仅显示当前节点资产",
|
||||
"CheckAssetsAmount": "校对资产数量",
|
||||
"ShowNodeInfo": "显示节点详情",
|
||||
"TestNodeAssetConnectivity": "测试资产节点可连接性",
|
||||
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
|
||||
},
|
||||
"users": {
|
||||
"UserName": "姓名",
|
||||
"Account": "账户",
|
||||
"Authentication": "认证",
|
||||
"Comment": "备注",
|
||||
@@ -801,6 +962,8 @@
|
||||
"Invite": "邀请",
|
||||
"InviteUserInOrg": "邀请用户加入此组织",
|
||||
"Guide": "向导",
|
||||
"setWeCom": "设置企业微信认证",
|
||||
"setDingTalk": "设置钉钉认证",
|
||||
"HelpText": {
|
||||
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
|
||||
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
|
||||
@@ -848,7 +1011,14 @@
|
||||
"resetSSHKey": "重置SSH密钥",
|
||||
"resetSSHKeySuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密钥邮件",
|
||||
"resetSSHKeyWarningMsg": "你确定要发送重置用户的SSH Key的邮件吗?",
|
||||
"resetWechat": "解绑企业微信",
|
||||
"resetWechatLoginWarningMsg": "你确定要解绑用户的 企业微信 吗?",
|
||||
"resetWechatLoginSuccessMsg": "重置成功, 用户可以重新绑定企业微信了",
|
||||
"resetDingTalk": "解绑钉钉",
|
||||
"resetDingTalkLoginWarningMsg": "你确定要解绑用户的 钉钉 吗?",
|
||||
"resetDingTalkLoginSuccessMsg": "重置成功, 用户可以重新绑定钉钉了",
|
||||
"send": "发送",
|
||||
"unbind": "解绑",
|
||||
"unblock": "解锁",
|
||||
"unblockSuccessMsg": "解锁成功",
|
||||
"unblockUser": "解锁用户"
|
||||
@@ -857,14 +1027,36 @@
|
||||
"tabs": {
|
||||
"assetPermissionRules": "资产授权规则",
|
||||
"databasePermissionRules": "数据库授权规则",
|
||||
"k8sPermissionRules": "Kubernetes授权规则",
|
||||
"grantedAssets": "授权的资产",
|
||||
"grantedK8Ss": "授权的Kubernetes",
|
||||
"grantedDatabases": "授权的数据库",
|
||||
"grantedRemoteApps": "授权的远程应用",
|
||||
"grantedApplications": "授权的应用",
|
||||
"ApplicationPermissionRules": "应用授权规则",
|
||||
"remoteAppPermissionRules": "远程应用授权规则"
|
||||
},
|
||||
"dateLastLogin": "最后登录日期",
|
||||
"needUpdatePasswordNextLogin": "下次登录须修改密码",
|
||||
"UpdatePassword": "更新密码",
|
||||
"SetPublicKey": "设置SSH公钥"
|
||||
"SetPublicKey": "设置SSH公钥",
|
||||
"passwordExpired": "密码过期了",
|
||||
"passwordWillExpiredPrefixMsg": "密码即将在 ",
|
||||
"passwordWillExpiredSuffixMsg": "天 后过期,请尽快修改您的密码。"
|
||||
},
|
||||
"notifications": {
|
||||
"MessageType": "消息类型",
|
||||
"Receivers": "接收人",
|
||||
"Subscription": "消息订阅",
|
||||
"ChangeReceiver": "修改消息接收人",
|
||||
"Subject": "主题",
|
||||
"Message": "消息",
|
||||
"DeliveryTime": "发送时间",
|
||||
"HasRead": "是否已读",
|
||||
"Sender": "发送人",
|
||||
"MarkAsRead": "标记已读",
|
||||
"NoUnreadMsg": "暂无未读消息",
|
||||
"SiteMessage": "站内信"
|
||||
},
|
||||
"xpack": {
|
||||
"Admin": "管理员",
|
||||
@@ -913,6 +1105,13 @@
|
||||
"Username": "用户名"
|
||||
},
|
||||
"Cloud": {
|
||||
"Aliyun": "阿里云",
|
||||
"Qcloud": "腾讯云",
|
||||
"AWS_China": "AWS(中国)",
|
||||
"AWS_Int": "AWS(国际)",
|
||||
"HuaweiCloud": "华为云",
|
||||
"Azure":"Azure(中国)",
|
||||
"Azure_Int": "Azure(国际)",
|
||||
"HostnameStrategy": "用于生成资产主机名。例如:1. 实例名称 (instanceDemo);2. 实例名称和部分IP(后两位) (instanceDemo-250.1)",
|
||||
"IsAlwaysUpdate": "资产信息保持最新",
|
||||
"AccountCreate": "创建账户",
|
||||
@@ -965,13 +1164,22 @@
|
||||
"GatherUserList": "收集用户",
|
||||
"GatherUserTaskCreate": "创建任务",
|
||||
"GatherUserTaskList": "任务列表",
|
||||
"GatherUserTaskUpdate": "更新任务"
|
||||
"GatherUserTaskUpdate": "更新任务",
|
||||
"GatherUserTaskDetail": "任务详情",
|
||||
"GatherUserTaskExecutionList": "任务执行列表"
|
||||
},
|
||||
"Import": "导入",
|
||||
"ImportLicense": "导入许可证",
|
||||
"ImportLicenseTip": "请导入许可证",
|
||||
"InterfaceSettings": "界面设置",
|
||||
"License": "许可证",
|
||||
"SystemMonitor": "系统监控",
|
||||
"ServiceRatio": "组件负载统计",
|
||||
"LoadStatus":"组件状态",
|
||||
"NormalLoad":"正常",
|
||||
"HighLoad":"较高",
|
||||
"CriticalLoad":"严重",
|
||||
"Offline": "离线",
|
||||
"LicenseDetail": "许可证详情",
|
||||
"LicenseFile": "许可证文件",
|
||||
"NoLicense": "暂无许可证",
|
||||
@@ -985,6 +1193,14 @@
|
||||
"DeleteOrgTitle": "请确保组织内的以下信息已删除",
|
||||
"DeleteOrgMsg": "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则",
|
||||
"OrgRole": "组织角色",
|
||||
"users_amount": "用户数量",
|
||||
"groups_amount": "用户组数量",
|
||||
"assets_amount": "资产数量",
|
||||
"admin_users_amount": "管理用户数量",
|
||||
"system_users_amount": "系统用户数量",
|
||||
"applications_amount": "应用数量",
|
||||
"asset_perms_amount": "资产授权数量",
|
||||
"app_perms_amount": "应用授权数量",
|
||||
"CreateOrgMsg": "请去组织详情内添加用户",
|
||||
"AddOrgMembers": "添加组织成员"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,55 @@
|
||||
{
|
||||
"": "",
|
||||
"accounts": {
|
||||
"PleaseClickLeftAssetToViewAssetAccount": "Asset account list, please click on the assets on the left to view",
|
||||
"PleaseClickLeftApplicationToViewApplicationAccount": "Application account list, please click on the application on the left to view",
|
||||
"PleaseClickLeftAssetToViewGatheredUser": "Gathered user list, please click on the assets on the left to view"
|
||||
},
|
||||
"acl": {
|
||||
"name": "Name",
|
||||
"username": "Username",
|
||||
"ip_group": "IP group",
|
||||
"action": "Action",
|
||||
"priority": "Priority",
|
||||
"date_created": "Date created",
|
||||
"created_by": "Created by",
|
||||
"asset": "Asset",
|
||||
"system_user": "System user",
|
||||
"username_group":"Username group",
|
||||
"hostname_group":"Hostname group",
|
||||
"asset_ip_group": "Asset ip group",
|
||||
"system_users_name_group": "Systemusers name group",
|
||||
"system_users_protocol_group": "Systemusers protocol group",
|
||||
"system_users_username_group": "systemusers username group",
|
||||
"apply_login_asset": "Apply login asset",
|
||||
"apply_login_system_user": "Apply login system user",
|
||||
"apply_login_user": "Apply login user",
|
||||
"RuleDetail": "Rule detail"
|
||||
},
|
||||
"applications": {
|
||||
"": "",
|
||||
"RemoteApp": "Remote app",
|
||||
"Database": "Database",
|
||||
"Cloud": "Cloud",
|
||||
"applicationsType": {
|
||||
"chrome": "Chrome",
|
||||
"mysql_workbench": "MySQL Workbench",
|
||||
"vmware_client":"Vmware Client",
|
||||
"custom":"Custom",
|
||||
"mysql": "MySQL",
|
||||
"oracle": "Oracle",
|
||||
"postgresql": "PostgreSQL",
|
||||
"mariadb": "MariaDB",
|
||||
"k8s": "kubernetes"
|
||||
},
|
||||
"applicationsCategory": {
|
||||
"remote_app": "Remote app",
|
||||
"db": "Database app",
|
||||
"cloud": "Cloud app"
|
||||
},
|
||||
"appPath": "App path",
|
||||
"appType": "App type",
|
||||
"appName": "App name",
|
||||
"asset": "Asset",
|
||||
"database": "Database",
|
||||
"host": "Host",
|
||||
@@ -32,15 +78,20 @@
|
||||
"Custom": "Custom",
|
||||
"cluster": "Cluster",
|
||||
"kubernetes":"Kubernetes",
|
||||
"clusterHelpTextMessage": "Tips: https://172.16.8.8:8443"
|
||||
"clusterHelpTextMessage": "Tips: https://172.16.8.8:8443",
|
||||
"DBInfo": "Database Info"
|
||||
},
|
||||
"assets": {
|
||||
"Action": "Action",
|
||||
"ActiveSelected": "Active selected",
|
||||
"AdminUser": "Admin user",
|
||||
"ReplaceNodeAssetsAdminUser":"Replace node assets admin user with this",
|
||||
"AdminUserDetail": "Admin user detail",
|
||||
"AdminUserListHelpMessage": "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, JumpServer users of the system using the user to `push system user`, `get assets hardware information`, etc.\n",
|
||||
"Asset": "Asset",
|
||||
"HardwareInfo": "Hardware info",
|
||||
"Hardware": "Hardware",
|
||||
"AccountList": "Account list",
|
||||
"AssetDetail": "Asset detail",
|
||||
"AssetList": "Asset list",
|
||||
"AssetListHelpMessage": "The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node\n",
|
||||
@@ -70,6 +121,8 @@
|
||||
"DateUpdated": "Date updated",
|
||||
"DeactiveSelected": "Deactive selected",
|
||||
"Disk": "Disk",
|
||||
"AdDomain": "AD Domain",
|
||||
"AdDomainHelpText": "AD domain provided to domain users for login",
|
||||
"Domain": "Domain",
|
||||
"DomainDetail": "Domain detail",
|
||||
"DomainHelpMessage": "The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.\nJMS => Domain gateway => Target assets",
|
||||
@@ -113,6 +166,7 @@
|
||||
"RefreshHardware": "Refresh hardware",
|
||||
"RemoteAppListHelpMessage": "Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application <b><a href='https://github.com/jumpserver/Jmservisor/releases'> Download application loader</a></b>",
|
||||
"RemoteApps": "Remote apps",
|
||||
"Applications": "Applications",
|
||||
"RemoteType": "Remote type",
|
||||
"RemoveFromCurrentNode": "Remove from node",
|
||||
"ReplaceNodeAssetsAdminUserWithThis": "Replace node assets admin user with this",
|
||||
@@ -129,7 +183,7 @@
|
||||
"TestAssetsConnective": "Test assets connective",
|
||||
"TestConnection": "Test connection",
|
||||
"Type": "Type",
|
||||
"UnselectedAssets": "Unselected assets",
|
||||
"UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
|
||||
"UnselectedNodes": "Unselected nodes",
|
||||
"UpdateAssetUserToken": "Update asset user auth",
|
||||
"Username": "Username",
|
||||
@@ -139,18 +193,25 @@
|
||||
"Version": "Version",
|
||||
"command_filter_list": "Command filter list",
|
||||
"date_joined": "Date joined",
|
||||
"sshKeyFingerprint": "SSH fingerprint",
|
||||
"ip": "IP",
|
||||
"sshkey": "sshkey",
|
||||
"GroupsHelpMessage": "Please fill in user groups, separated by commas if there are multiple user groups(Please fill in the existing user groups)",
|
||||
"HomeHelpMessage": "Default home directory: /home/system username",
|
||||
"Home": "Home",
|
||||
"LinuxUserAffiliateGroup": "Linux user affiliate group",
|
||||
"ipDomain": "IP(Domain)"
|
||||
"ipDomain": "IP(Domain)",
|
||||
"HostProtocol": "Host Protocol",
|
||||
"DatabaseProtocol": "Database Protocol",
|
||||
"OtherProtocol": "Other Protocol",
|
||||
"PasswordOrToken": "Password / Token"
|
||||
},
|
||||
"audits": {
|
||||
"Hosts": "Host",
|
||||
"RunUser": "Run user",
|
||||
"User": "User",
|
||||
"Username": "Username",
|
||||
"SystemUserName": "System username",
|
||||
"View": "View"
|
||||
},
|
||||
"auth": {
|
||||
@@ -158,20 +219,30 @@
|
||||
"ReLogin": "Re-Login"
|
||||
},
|
||||
"common": {
|
||||
"ConnectWebSocketError": "Connect Websocket failed",
|
||||
"Nothing": "Nothing",
|
||||
"Action": "Action",
|
||||
"CustomCol":"Custom table col",
|
||||
"RequestTickets": "Request tickets",
|
||||
"Actions": "Actions",
|
||||
"NeedSpecifiedFile": "Required to upload the specified format file",
|
||||
"TestPortErrorMsg":"Port Error, please check",
|
||||
"Activate": "Activate",
|
||||
"actionsTips":"Clipboard's copy and paste control only support RDP/VNC protocol.",
|
||||
"Active": "Active",
|
||||
"TableColSettingInfo": "Please select the list details you want to display",
|
||||
"Add": "Add",
|
||||
"PleaseAgreeToTheTerms": "Please agree to the terms",
|
||||
"PushSelected":"Push selected",
|
||||
"UpdateAssetDetail": "Update more detail",
|
||||
"AddSuccessMsg": "Add success",
|
||||
"Auth": "Authorization",
|
||||
"BadRequestErrorMsg": "Bad request, please check again",
|
||||
"BadRoleErrorMsg": "Bad request, no permission for this operation",
|
||||
"BadConflictErrorMsg": "Refreshing, please try again later",
|
||||
"Basic": "Basic",
|
||||
"BasicInfo": "Basic info",
|
||||
"ApplyInfo": "Apply info",
|
||||
"Cancel": "Cancel",
|
||||
"Close": "Close",
|
||||
"Command filter": "Command filter",
|
||||
@@ -181,6 +252,7 @@
|
||||
"CreatedBy": "Created by",
|
||||
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set,give priority to Regularly perform",
|
||||
"DateEnd": "End date",
|
||||
"Resource": "Resource",
|
||||
"DateLast24Hours": "Last 24 hours",
|
||||
"DateLast3Months": "Last 3 months",
|
||||
"DateLastMonth": "Last month",
|
||||
@@ -194,10 +266,16 @@
|
||||
"EnterForSearch": "Press enter to search",
|
||||
"Export": "Export",
|
||||
"Import": "Import",
|
||||
"ContinueImport": "ContinueImport",
|
||||
"Continue": "Continue",
|
||||
"Stop": "Stop",
|
||||
"Finished": "Finished",
|
||||
"Refresh": "Refresh",
|
||||
"Info": "Info",
|
||||
"MFAConfirm": "MFA Confirm",
|
||||
"MFARequireForSecurity": "MFA required for security",
|
||||
"PasswordConfirm": "Password Confirm",
|
||||
"PasswordRequireForSecurity": "Password required for security",
|
||||
"Members": "Members",
|
||||
"More": "More",
|
||||
"Message": "Message",
|
||||
@@ -209,20 +287,26 @@
|
||||
"Other": "Other",
|
||||
"Others": "Others",
|
||||
"Push": "Push",
|
||||
"Receivers": "Receivers",
|
||||
"QuickUpdate": "Quick update",
|
||||
"RemoveSuccessMsg": "Remove success",
|
||||
"Reset": "Reset",
|
||||
"Search": "Search",
|
||||
"MFAErrorMsg": "MFA Error,please check",
|
||||
"InputEmailAddress": "Please enter your email address",
|
||||
"Select": "Select",
|
||||
"SelectFile": "Select file",
|
||||
"Show": "Show",
|
||||
"Submit": "Submit",
|
||||
"Test": "Test",
|
||||
"SaveAndAddAnother":"Save and add another",
|
||||
"TestSuccessMsg": "Test Success",
|
||||
"To": "To",
|
||||
"Update": "Update",
|
||||
"bind": "Bind",
|
||||
"unbind": "Unbind",
|
||||
"Upload": "Upload",
|
||||
"Clone": "Clone",
|
||||
"Username": "Username",
|
||||
"Validity": "Validity",
|
||||
"Invalidity": "Invalidity",
|
||||
@@ -236,8 +320,10 @@
|
||||
"bulkRemoveSuccessMsg": "Bulk remove success",
|
||||
"NeedAssetsAndSystemUserErrMsg": "Need assets and systemuser",
|
||||
"createBy": "Create by",
|
||||
"cloneFrom": "Clone from",
|
||||
"createErrorMsg": "Create error",
|
||||
"createSuccessMsg": "Create success",
|
||||
"saveSuccessContinueMsg": "Create success, you may add another",
|
||||
"createdBy": "Created by",
|
||||
"dateCreated": "Date created",
|
||||
"dateFinished": "Date finished",
|
||||
@@ -251,6 +337,12 @@
|
||||
"disableSelected": "Disable selected",
|
||||
"fieldRequiredError": "This field is required",
|
||||
"getErrorMsg": "Get failed",
|
||||
"fileType": "File type",
|
||||
"Status": "Status",
|
||||
"Total": "Total",
|
||||
"Success": "Success",
|
||||
"Failed": "Failed",
|
||||
"Pending": "Pending",
|
||||
"imExport": {
|
||||
"ExportAll": "Export all",
|
||||
"ExportOnlyFiltered": "Export only filtered",
|
||||
@@ -260,7 +352,10 @@
|
||||
"downloadImportTemplateMsg": "Download import template",
|
||||
"downloadUpdateTemplateMsg": "Download update template",
|
||||
"onlyCSVFilesTips": "Only csv supported",
|
||||
"updateSuccessMsg": "Update success, total: {count}"
|
||||
"updateSuccessMsg": "Update success, total: {count}",
|
||||
"dragUploadFileInfo": "Drag file here or click to upload",
|
||||
"uploadCsvLth10MHelpText": "csv/xlsx files with a size less than 10M",
|
||||
"hasImportErrorItemMsg": "There is an error item, click the x icon to view the details, and continue to import after editing"
|
||||
},
|
||||
"isValid": "Is valid",
|
||||
"nav": {
|
||||
@@ -296,7 +391,9 @@
|
||||
"NUMBER_REQUIRED": "Number required",
|
||||
"SPECIAL_CHAR_REQUIRED": "Special char required",
|
||||
"MIN_LENGTH_ERROR": "Password minimum length {}"
|
||||
}
|
||||
},
|
||||
"lastCannotBeDeleteMsg": "The last one can't be delete",
|
||||
"InvalidJson": "Not a valid json format"
|
||||
},
|
||||
"dashboard": {
|
||||
"ActiveAsset": "Asset active",
|
||||
@@ -387,15 +484,20 @@
|
||||
"SystemUser": "System user",
|
||||
"User": "User",
|
||||
"UserGroups": "UserGroups",
|
||||
"DatabaseAppPermission": "Databases permissions",
|
||||
"RemoteAppPermission": "Remote apps permissions",
|
||||
"KubernetesAppPermission": "Kubernetes permissions",
|
||||
"addAssetToThisPermission": "Add asset to this permission",
|
||||
"addDatabaseAppToThisPermission": "Add DatabaseApp to this permission",
|
||||
"addK8sAppToThisPermission": "Add KubernetesApp to this permission",
|
||||
"addApplicationToThisPermission": "Add Application to this permission",
|
||||
"addNodeToThisPermission": "Add node to this permission",
|
||||
"addRemoteAppToThisPermission": "Add RemoteApp to this permission",
|
||||
"addSystemUserToThisPermission": "System user",
|
||||
"addUserGroupToThisPermission": "Add user group to this permission",
|
||||
"addUserToThisPermission": "Add user to this permission",
|
||||
"all": "All",
|
||||
"PermName":"Perm name",
|
||||
"assetAndNode": "Assets and node",
|
||||
"assetCount": "Asset count",
|
||||
"connect": "Connect",
|
||||
@@ -405,12 +507,15 @@
|
||||
"downloadFile": "Download file",
|
||||
"hostName": "Hostname",
|
||||
"isValid": "Validity",
|
||||
"isEffective": "Effective",
|
||||
"nodeCount": "Node count",
|
||||
"refreshFail": "Refresh fail",
|
||||
"refreshPermissionCache": "Refresh permission cache",
|
||||
"refreshSuccess": "Refresh success",
|
||||
"remoteApp": "RemoteApp",
|
||||
"remoteAppCount": "RemoteApp count",
|
||||
"appsCount": "App count",
|
||||
"appsList":"App list",
|
||||
"DatabaseAppCount": "DatabaseApp count",
|
||||
"KubernetesAppCount": "KubernetesApp count",
|
||||
"systemUserCount": "System user count",
|
||||
@@ -425,29 +530,33 @@
|
||||
},
|
||||
"route": {
|
||||
"": "",
|
||||
"Accounts": "Accounts",
|
||||
"AssetAccount": "Asset account",
|
||||
"ApplicationAccount": "Application account",
|
||||
"Ticket": "Tickets",
|
||||
"CommandConfirm": "Command confirm",
|
||||
"AdminUserCreate": "Admin user create",
|
||||
"AdminUserDetail": "Admin user detail",
|
||||
"AdminUserList": "Admin users",
|
||||
"AdminUserList": "Admin Users",
|
||||
"AdminUserUpdate": "Admin user update",
|
||||
"Applications": "Applications",
|
||||
"AssetCreate": "Asset create",
|
||||
"AssetDetail": "Asset detail",
|
||||
"AssetList": "Assets",
|
||||
"AssetPermission": "Asset permissions",
|
||||
"AssetPermission": "Asset Permissions",
|
||||
"AssetPermissionCreate": "Asset permissions create",
|
||||
"AssetPermissionDetail": "Asset permissions detail",
|
||||
"AssetPermissionUpdate": "Asset permissions update",
|
||||
"AssetUpdate": "Asset update",
|
||||
"Assets": "Assets",
|
||||
"Audits": "Audits",
|
||||
"BatchCommand": "Batch command",
|
||||
"BatchCommandLog": "Batch command log",
|
||||
"BatchCommand": "Batch Command",
|
||||
"BatchCommandLog": "Batch Command Log",
|
||||
"CeleryTaskLog": "Celery task log",
|
||||
"CommandExecutions": "CommandExecutions ",
|
||||
"CommandFilterCreate": "Command filter create",
|
||||
"CommandFilterDetail": "Command filter detail",
|
||||
"CommandFilterList": "Command filters",
|
||||
"CommandFilterList": "Command Filters",
|
||||
"CommandFilterRulesCreate": "Command filter rules create",
|
||||
"CommandFilterRulesUpdate": "Command filter rules update",
|
||||
"CommandFilterUpdate": "Command filter update",
|
||||
@@ -456,7 +565,7 @@
|
||||
"CreateCommandStorage": "Create command storage",
|
||||
"CreateReplayStorage": "Create replay storage",
|
||||
"Dashboard": "Dashboard",
|
||||
"DatabaseApp": "Database apps",
|
||||
"DatabaseApp": "Database Apps",
|
||||
"DatabaseAppCreate": "Database app create",
|
||||
"DatabaseAppDetail": "Database app detail",
|
||||
"DatabaseAppPermission": "Databases permissions",
|
||||
@@ -464,7 +573,7 @@
|
||||
"DatabaseAppPermissionDetail": "Databases permissions detail",
|
||||
"DatabaseAppPermissionUpdate": "Databases permissions update",
|
||||
"DatabaseAppUpdate": "Database app update",
|
||||
"KubernetesApp": "Kubernetes apps",
|
||||
"KubernetesApp": "Kubernetes Apps",
|
||||
"KubernetesAppCreate": "Kubernetes app create",
|
||||
"KubernetesAppDetail": "Kubernetes app detail",
|
||||
"KubernetesAppPermission": "Kubernetes permissions",
|
||||
@@ -472,35 +581,50 @@
|
||||
"KubernetesAppPermissionDetail": "Kubernetes permissions detail",
|
||||
"KubernetesAppPermissionUpdate": "Kubernetes permissions update",
|
||||
"KubernetesAppUpdate": "Kubernetes app update",
|
||||
"Acl": "Access Control",
|
||||
"UserAclList": "User acl list",
|
||||
"UserAclCreate": "User acl create",
|
||||
"UserAclUpdate": "User acl update",
|
||||
"UserAclLists": "User acl lists",
|
||||
"UserAclDetail": "User acl detail",
|
||||
"AssetAclList": "Asset Acl",
|
||||
"AssetAclCreate": "Asset acl create",
|
||||
"AssetAclUpdate": "Asset acl update",
|
||||
"AssetAclDetail": "Asset acl detail",
|
||||
"DomainCreate": "Domain create",
|
||||
"DomainDetail": "Domain detail",
|
||||
"DomainList": "Domains",
|
||||
"DomainUpdate": "Domain update",
|
||||
"FileManager": "File manager",
|
||||
"FtpLog": "FTP logs",
|
||||
"FileManager": "File Manager",
|
||||
"FtpLog": "FTP Logs",
|
||||
"GatewayCreate": "Gateway create",
|
||||
"GatewayUpdate": "Gateway update",
|
||||
"JobCenter": "Jobcenter",
|
||||
"LabelCreate": "Label create",
|
||||
"LabelList": "Labels",
|
||||
"LabelUpdate": "Label update",
|
||||
"LoginLog": "Login logs",
|
||||
"MyApps": "My apps",
|
||||
"MyAssets": "My assets",
|
||||
"OperateLog": "Operation logs",
|
||||
"PasswordChangeLog": "Password update logs",
|
||||
"LoginLog": "Login Logs",
|
||||
"MyApps": "My Apps",
|
||||
"MyAssets": "My Assets",
|
||||
"OperateLog": "Operation Logs",
|
||||
"PasswordChangeLog": "Password Update Logs",
|
||||
"Perms": "Permissions",
|
||||
"PersonalInformationImprovement": "PersonalInformationImprovement",
|
||||
"PlatformCreate": "Platform create",
|
||||
"PlatformDetail": "Platform detail",
|
||||
"PlatformList": "Platforms",
|
||||
"PlatformUpdate": "Platform update",
|
||||
"RemoteApp": "Remote apps",
|
||||
"RemoteApp": "Remote Apps",
|
||||
"RemoteAppDetail": "Remote app detail",
|
||||
"RemoteAppPermission": "Remote apps permissions",
|
||||
"ApplicationPermission": "Application Permissions",
|
||||
"RemoteAppPermissionCreate": "Remote apps permission create",
|
||||
"RemoteAppPermissionDetail": "Remote apps permissions detail",
|
||||
"RemoteAppPermissionUpdate": "Remote app permission update",
|
||||
"ApplicationDetail": "Application detail",
|
||||
"ApplicationPermissionCreate": "Application permission create",
|
||||
"ApplicationPermissionDetail": "Application permission detail",
|
||||
"ApplicationPermissionUpdate": "Application permission update",
|
||||
"RemoteAppUpdate": "Remote app update",
|
||||
"ReplayStorageUpdate": "Replay storage update",
|
||||
"SessionDetail": "Sessions detail",
|
||||
@@ -510,11 +634,11 @@
|
||||
"Settings": "Settings",
|
||||
"SystemUserCreate": "System user create",
|
||||
"SystemUserDetail": "System user detail",
|
||||
"SystemUserList": "System users",
|
||||
"SystemUserList": "System Users",
|
||||
"SystemUserUpdate": "System user update",
|
||||
"TaskDetail": "Tasks detail",
|
||||
"TaskList": "Tasks",
|
||||
"TaskMonitor": "Task monitor",
|
||||
"TaskMonitor": "Task Monitor",
|
||||
"Terminal": "Terminal",
|
||||
"TicketDetail": "Ticket detail",
|
||||
"TicketCreate": "Ticket create",
|
||||
@@ -524,7 +648,7 @@
|
||||
"UserFirstLogin": "UserFirstLogin",
|
||||
"UserGroupCreate": "User group create",
|
||||
"UserGroupDetail": "User group detail",
|
||||
"UserGroupList": "User groups",
|
||||
"UserGroupList": "User Groups",
|
||||
"UserGroupUpdate": "User group update",
|
||||
"UserGuide": "UserGuide",
|
||||
"UserList": "Users",
|
||||
@@ -532,7 +656,9 @@
|
||||
"UserUpdate": "User update",
|
||||
"Users": "Users",
|
||||
"WebFTP": "WebFTP",
|
||||
"WebTerminal": "Web terminal"
|
||||
"WebTerminal": "Web Terminal",
|
||||
"Notifications": "Notifications",
|
||||
"SiteMessageList": "Site message"
|
||||
},
|
||||
"sessions": {
|
||||
"StorageConfiguration": "Storage configuration",
|
||||
@@ -557,6 +683,11 @@
|
||||
"duration": "Duration",
|
||||
"endPoint": "Endpoint",
|
||||
"endpointSuffix": "Endpoint suffix",
|
||||
"sessionActiveCount": "session active count",
|
||||
"systemCpuLoad": "cpu load",
|
||||
"systemDiskUsedPercent": "disk used percent",
|
||||
"systemMemoryUsedPercent": "memory used percent",
|
||||
"EsDisabled": "Node is unavailable, please contact administrator",
|
||||
"go": "Go",
|
||||
"goto": "Goto",
|
||||
"hosts": "Hosts",
|
||||
@@ -590,6 +721,7 @@
|
||||
"common": "common"
|
||||
},
|
||||
"Monitor": "Monitor",
|
||||
"XRDPNotSupport": "RDP Client session not support now",
|
||||
"sessionMonitor": "Session Monitor",
|
||||
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
|
||||
"helpText": {
|
||||
@@ -601,6 +733,7 @@
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"InsecureCommandNotifyToSubscription": "Insecure command notification setting, change to system message subscription, support more notify method",
|
||||
"ApiKeyList": "Api key list",
|
||||
"AssetCount": "Asset count",
|
||||
"Basic": "Basic setting",
|
||||
@@ -620,6 +753,8 @@
|
||||
"Security": "Security setting",
|
||||
"SecuritySetting": "Security setting",
|
||||
"SubscriptionID": "Subscription ID",
|
||||
"SystemMessageSubscription": "System messages",
|
||||
"insecureCommandEmailUpdate": "Setting",
|
||||
"Terminal": "Terminal setting",
|
||||
"all": "All",
|
||||
"authLdap": "Enable LDAP auth",
|
||||
@@ -649,6 +784,10 @@
|
||||
"emailTest": "Test connection",
|
||||
"emailUserSSL": "Use SSL",
|
||||
"emailUserTLS": "Use TLS",
|
||||
"MailSend": "Mail send",
|
||||
"LDAPServerInfo": "LDAP Server",
|
||||
"LDAPUser": "LDAP User",
|
||||
"InsecureCommandAlert": "Insecure command alert",
|
||||
"helpText": {
|
||||
"ApiKeyList": "The API key is used to sign the request header. The header of each request is different. Please refer to the usage documentation",
|
||||
"authLdapSearchFilter": "Choice may be (cn|uid|sAMAccountName)=%(user)s)",
|
||||
@@ -725,23 +864,32 @@
|
||||
"refreshLdapCache":"Refreshing Ldap cache ",
|
||||
"LicenseExpired": "License expired",
|
||||
"LicenseWillBe": "License will expire at ",
|
||||
"Expire": ""
|
||||
},
|
||||
"settings": {
|
||||
"Expire": "",
|
||||
"WeCom": "WeCom",
|
||||
"DingTalk": "DingTalk",
|
||||
"dingTalkTest": "Test",
|
||||
"weComTest": "Test",
|
||||
"setting": "Setting"
|
||||
},
|
||||
|
||||
"tickets": {
|
||||
"PermissionName": "Permission name",
|
||||
"Accept": "Accept",
|
||||
"AssignedMe": "Assigned me",
|
||||
"Assignee": "Assignee",
|
||||
"RequestPerm":"Request Perm",
|
||||
"AssignedInfo":"Assigned Info",
|
||||
"OpenTicket": "Open Ticket",
|
||||
"HandleTicket": "Handle Ticket",
|
||||
"FinishedTicket": "Finished Ticket",
|
||||
"Assignees": "Assignees",
|
||||
"Close": "Close",
|
||||
"OpenStatus":"Open",
|
||||
"CloseStatus":"Close",
|
||||
"Comment": "Comment",
|
||||
"MyTickets": "My tickets",
|
||||
"action": "Action",
|
||||
"IPGroup": "IP 组",
|
||||
"Reject": "Reject",
|
||||
"date": "Date",
|
||||
"reply": "Reply",
|
||||
@@ -751,19 +899,29 @@
|
||||
"user": "User",
|
||||
"Status": "Status",
|
||||
"Open": "Open",
|
||||
"OrgName":"Org name",
|
||||
"IP": "IP",
|
||||
"Hostname": "Hostname",
|
||||
"Asset": "Asset",
|
||||
"SystemUser": "System user",
|
||||
"Applicant": "Applicant",
|
||||
"RequestAssetPerm": "Request asset perm",
|
||||
"RequestApplicationPerm": "Request application perm",
|
||||
"Pending": "Open",
|
||||
"Approved": "Approved",
|
||||
"Rejected": "Rejected",
|
||||
"Closed": "Closed",
|
||||
"helpText": {
|
||||
"ips": "Enter the IP address group, separated by commas",
|
||||
"fuzzySearch": "Support for fuzzy search"
|
||||
}
|
||||
"fuzzySearch": "Support for fuzzy search",
|
||||
"application": "Enter the application group, separated by commas"
|
||||
},
|
||||
"ApplyRunUser": "Apply run user",
|
||||
"ApplyRunAsset": "Apply run asset",
|
||||
"ApplyRunSystemUser": "Apply run system user",
|
||||
"ApplyRunCommand": "Apply run command",
|
||||
"ApplyFromSession": "Session",
|
||||
"ApplyFromCMDFilterRule": "Command filter rule"
|
||||
},
|
||||
"tree": {
|
||||
"AddAssetToNode": "Add asset to node",
|
||||
@@ -773,11 +931,13 @@
|
||||
"RenameNode": "Rename node",
|
||||
"ShowAssetAllChildrenNode": "Show asset all children node",
|
||||
"ShowAssetOnlyCurrentNode": "Show asset only current node",
|
||||
"CheckAssetsAmount": "Check assets amount",
|
||||
"ShowNodeInfo": "Show node information",
|
||||
"TestNodeAssetConnectivity": "Test node asset connectivity",
|
||||
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
|
||||
},
|
||||
"users": {
|
||||
"UserName": "Name",
|
||||
"Account": "Account",
|
||||
"Existing":"Existing",
|
||||
"Authentication": "Account",
|
||||
@@ -787,6 +947,8 @@
|
||||
"DateJoined": "Date joined",
|
||||
"DateLastLogin": "Date last login",
|
||||
"DatePasswordLastUpdated": "Date password last updated",
|
||||
"setWeCom": "Set wecom login",
|
||||
"setDingTalk": "Set dingtalk login",
|
||||
"DatePasswordUpdated": "Date password updated",
|
||||
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
|
||||
"Email": "Email",
|
||||
@@ -846,7 +1008,11 @@
|
||||
"resetSSHKey": "Reset SSH key",
|
||||
"resetSSHKeySuccessMsg": "An e-mail has been sent to the user`s mailbox",
|
||||
"resetSSHKeyWarningMsg": "This will reset the user public key and send a reset mail",
|
||||
"resetWechat": "Reset Wechat",
|
||||
"resetWechatLoginWarningMsg": "This will reset the user Wechat setting, user can reset it",
|
||||
"resetWechatLoginSuccessMsg": "Reset Wechat success",
|
||||
"send": "Send",
|
||||
"unbind": "Unbind",
|
||||
"unblock": "Unblock",
|
||||
"unblockSuccessMsg": "Account has unblocked",
|
||||
"unblockUser": "Unblock login"
|
||||
@@ -855,13 +1021,36 @@
|
||||
"tabs": {
|
||||
"assetPermissionRules": "Asset permission rules",
|
||||
"databasePermissionRules": "Database Permission rules",
|
||||
"k8sPermissionRules": "Kubernetes Permission rules",
|
||||
"grantedAssets": "Granted assets",
|
||||
"grantedK8Ss":"Granted K8Ss",
|
||||
"grantedDatabases": "Granted databases",
|
||||
"grantedRemoteApps": "Granted remote apps",
|
||||
"grantedApplications": "Granted applications",
|
||||
"ApplicationPermissionRules": "Application permission rules",
|
||||
"remoteAppPermissionRules": "Remote app permission rules"
|
||||
},
|
||||
"UpdatePassword": "",
|
||||
"UpdatePublicKey": ""
|
||||
"needUpdatePasswordNextLogin": "Update password next login",
|
||||
"UpdatePassword": "Update password",
|
||||
"SetPublicKey": "Set public key",
|
||||
"UpdatePublicKey": "",
|
||||
"passwordExpired": "Password expired",
|
||||
"passwordWillExpiredPrefixMsg": "The password will expire in ",
|
||||
"passwordWillExpiredSuffixMsg": " days.Please change your password as soon as possible."
|
||||
},
|
||||
"notifications": {
|
||||
"MessageType": "Message Type",
|
||||
"Receivers": "Receivers",
|
||||
"Subscription": "Subscription",
|
||||
"ChangeReceiver": "Change Receivers",
|
||||
"Subject": "Subject",
|
||||
"Message": "Message",
|
||||
"DeliveryTime": "Delivery time",
|
||||
"HasRead": "Has read",
|
||||
"Sender": "Sender",
|
||||
"MarkAsRead": "Mark as read",
|
||||
"NoUnreadMsg": "No unread messages",
|
||||
"SiteMessage": "Site messages"
|
||||
},
|
||||
"xpack": {
|
||||
"Admin": "Admin",
|
||||
@@ -874,7 +1063,7 @@
|
||||
"Asset": "Asset",
|
||||
"AssetAmount": "Asset",
|
||||
"AssetAndNode": "Asset and Node",
|
||||
"ChangeAuthPlan": "Change auth plan",
|
||||
"ChangeAuthPlan": "Change Auth Plan",
|
||||
"ChangeAuthPlanCreate": "Create change auth plan",
|
||||
"ChangeAuthPlanUpdate": "Update change auth plan",
|
||||
"CyclePerform": "Cycle perform",
|
||||
@@ -910,6 +1099,13 @@
|
||||
"Username": "Username"
|
||||
},
|
||||
"Cloud": {
|
||||
"Aliyun": "Ali Cloud",
|
||||
"Qcloud": "Tencent Cloud",
|
||||
"AWS_China": "AWS(China)",
|
||||
"AWS_Int": "AWS(International)",
|
||||
"HuaweiCloud": "Huawei Cloud",
|
||||
"Azure":"Azure(China)",
|
||||
"Azure_Int": "Azure(International)",
|
||||
"HostnameStrategy": "Used to produce the asset hostname. For example, 1. Instance name (instanceDemo);2. Instance name and Partial IP (instanceDemo-250.1)",
|
||||
"IsAlwaysUpdate": "Asset info is kept up-to-date",
|
||||
"AccountCreate": "Create account",
|
||||
@@ -917,7 +1113,7 @@
|
||||
"AccountUpdate": "Update account",
|
||||
"AccountDetail": "Account detail",
|
||||
"Cloud": "Cloud center",
|
||||
"CloudCenter": "Cloud center",
|
||||
"CloudCenter": "Cloud Center",
|
||||
"Provider": "Provider",
|
||||
"Validity": "Validity",
|
||||
"IsAlwaysUpdateHelpTips": "Whether the asset information, including Hostname, IP, Platform, and AdminUser, is updated synchronously each time a synchronization task is performed",
|
||||
@@ -958,18 +1154,27 @@
|
||||
"Execute": "Execute",
|
||||
"Expired": "Expired",
|
||||
"GatherUser": {
|
||||
"GatherUser": "Gather user",
|
||||
"GatherUser": "Gather User",
|
||||
"GatherUserList": "Gather user",
|
||||
"GatherUserTaskCreate": "Create gather user task",
|
||||
"GatherUserTaskList": "Gather user task list",
|
||||
"GatherUserTaskUpdate": "Update gather user task"
|
||||
"GatherUserTaskUpdate": "Update gather user task",
|
||||
"GatherUserTaskDetail": "Gather user detail",
|
||||
"GatherUserTaskExecutionList": "Gather user task execution list"
|
||||
},
|
||||
"Import": "Import",
|
||||
"ImportLicense": "Import license",
|
||||
"ImportLicenseTip": "Please Import License",
|
||||
"InterfaceSettings": "Interface setting",
|
||||
"InterfaceSettings": "Interface Setting",
|
||||
"License": "License",
|
||||
"LicenseDetail": "License detail",
|
||||
"SystemMonitor": "System Monitor",
|
||||
"ServiceRatio": "Service ratio",
|
||||
"LoadStatus":"Status",
|
||||
"NormalLoad":"Normal",
|
||||
"HighLoad":"High",
|
||||
"Offline": "Offline",
|
||||
"CriticalLoad":"Critical",
|
||||
"LicenseFile": "License file",
|
||||
"NoLicense": "No License",
|
||||
"Node": "Node",
|
||||
@@ -983,7 +1188,15 @@
|
||||
"DeleteOrgMsg":"User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission",
|
||||
"OrgRole": "Org role",
|
||||
"CreateOrgMsg": "Please go to Organization Details to add users",
|
||||
"AddOrgMembers": "Add organization members"
|
||||
"AddOrgMembers": "Add organization members",
|
||||
"users_amount": "Users amount",
|
||||
"groups_amount": "Groups amount",
|
||||
"assets_amount": "Assets amount",
|
||||
"admin_users_amount": "Admin users amount",
|
||||
"system_users_amount": "System users amount",
|
||||
"applications_amount": "Applications amount",
|
||||
"asset_perms_amount": "Asset perms amount",
|
||||
"app_perms_amount": "App perms amount"
|
||||
},
|
||||
"RestoreButton": "Restore Default",
|
||||
"SubscriptionID": "Subscription ID",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="footer" :style="style">
|
||||
<div class="pull-right">
|
||||
Version <strong>2.0.2</strong> <span v-if="!publicSettings.XPACK_LICENSE_IS_VALID"> GPLv2. </span>
|
||||
Version <strong> dev </strong> <span v-if="!publicSettings.XPACK_LICENSE_IS_VALID"> GPLv2. </span>
|
||||
</div>
|
||||
<div v-if="!publicSettings.XPACK_LICENSE_IS_VALID" style="padding-left:20px;">
|
||||
<strong>Copyright</strong> FIT2CLOUD 飞致云 © 2014-2020
|
||||
<strong>Copyright</strong> FIT2CLOUD 飞致云 © 2014-2021
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
:method="method"
|
||||
:form="form"
|
||||
:url="iUrl"
|
||||
:has-save-continue="iHasSaveContinue"
|
||||
:has-reset="iHasReset"
|
||||
:is-submitting="isSubmitting"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@@ -19,44 +21,64 @@ export default {
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
// 创建对象的地址
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 更新的对象
|
||||
object: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
// form的默认值
|
||||
initial: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
afterGetFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
},
|
||||
// 提交前,清理form的值
|
||||
cleanFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
},
|
||||
// 当提交的时候,怎么处理
|
||||
onSubmit: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// 如何提交数据
|
||||
performSubmit: {
|
||||
type: Function,
|
||||
default(validValues) {
|
||||
return this.$axios[this.method](this.iUrl, validValues)
|
||||
}
|
||||
},
|
||||
// 创建成功的msg
|
||||
createSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.createSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 保存成功,继续添加的msg
|
||||
saveSuccessContinueMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.saveSuccessContinueMsg')
|
||||
}
|
||||
},
|
||||
// 更新成功的msg
|
||||
updateSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.updateSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 创建成功的跳转路由
|
||||
createSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
@@ -64,6 +86,7 @@ export default {
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
// 更新成功的跳转路由
|
||||
updateSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
@@ -71,12 +94,23 @@ export default {
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
objectDetailRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name
|
||||
.replace('Update', 'Detail')
|
||||
.replace('Create', 'Detail')
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
// 获取下一个路由
|
||||
getNextRoute: {
|
||||
type: Function,
|
||||
default(res, method) {
|
||||
return method === 'post' ? this.createSuccessNextRoute : this.updateSuccessNextRoute
|
||||
}
|
||||
},
|
||||
// 获取提交的方法
|
||||
getMethod: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
@@ -88,6 +122,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// 获取创建和更新的url function
|
||||
getUrl: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
@@ -101,12 +136,47 @@ export default {
|
||||
},
|
||||
onPerformSuccess: {
|
||||
type: Function,
|
||||
default(res, method, vm) {
|
||||
const msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
|
||||
default(res, method, vm, addContinue) {
|
||||
let msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
|
||||
if (addContinue) {
|
||||
msg = this.saveSuccessContinueMsg
|
||||
}
|
||||
let msgLinkName = this.$t('common.Resource')
|
||||
if (res.name) {
|
||||
msgLinkName = res.name
|
||||
} else if (res.hostname) {
|
||||
msgLinkName = res.hostname
|
||||
}
|
||||
const detailRoute = this.objectDetailRoute
|
||||
detailRoute['params'] = { 'id': res.id }
|
||||
const route = this.getNextRoute(res, method)
|
||||
this.$emit('submitSuccess', res)
|
||||
this.$message.success(msg)
|
||||
setTimeout(() => this.$router.push(route), 100)
|
||||
const h = this.$createElement
|
||||
this.$log.debug('router is: ', detailRoute)
|
||||
if (this.hasDetailInMsg) {
|
||||
this.$message({
|
||||
message: h('p', null, [
|
||||
h('el-link', {
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
},
|
||||
style: { 'vertical-align': 'top' }
|
||||
}, msgLinkName),
|
||||
h('span', { style: {
|
||||
'padding-left': '5px',
|
||||
'height': '18px',
|
||||
'line-height': '18px',
|
||||
'font-size': '13.5px',
|
||||
'font-weight': ' 400' }}, msg)
|
||||
]),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.success(msg)
|
||||
}
|
||||
if (!addContinue) {
|
||||
setTimeout(() => this.$router.push(route), 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
onPerformError: {
|
||||
@@ -125,13 +195,22 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hasSaveContinue: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
hasDetailInMsg: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
loading: true,
|
||||
isSubmitting: false
|
||||
isSubmitting: false,
|
||||
clone: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -140,44 +219,76 @@ export default {
|
||||
},
|
||||
iUrl() {
|
||||
return this.getUrl()
|
||||
},
|
||||
iHasSaveContinue() {
|
||||
if (this.hasSaveContinue != null) {
|
||||
return this.hasSaveContinue
|
||||
}
|
||||
return this.method === 'post'
|
||||
},
|
||||
iHasReset() {
|
||||
if (this.hasReset != null) {
|
||||
return this.hasReset
|
||||
}
|
||||
return this.method === 'put'
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$log.debug('Object init is: ', this.object)
|
||||
this.loading = true
|
||||
try {
|
||||
const values = await this.getFormValue()
|
||||
this.form = Object.assign(this.form, values)
|
||||
this.$log.debug('Final object is: ', values)
|
||||
const formValue = Object.assign(this.form, values)
|
||||
this.form = this.afterGetFormValue(formValue)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmit(values) {
|
||||
handleSubmit(values, formName, addContinue) {
|
||||
let handler = this.onSubmit || this.defaultOnSubmit
|
||||
handler = handler.bind(this)
|
||||
values = this.cleanFormValue(values)
|
||||
return handler(values)
|
||||
return handler(values, formName, addContinue)
|
||||
},
|
||||
defaultOnSubmit(validValues) {
|
||||
defaultOnSubmit(validValues, formName, addContinue) {
|
||||
this.isSubmitting = true
|
||||
this.performSubmit(validValues)
|
||||
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this))
|
||||
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch((error) => this.onPerformError(error, this.method, this))
|
||||
.finally(() => { this.isSubmitting = false })
|
||||
},
|
||||
async getFormValue() {
|
||||
if (this.method !== 'put') {
|
||||
const cloneFrom = this.$route.query['clone_from']
|
||||
if (this.method !== 'put' && !cloneFrom) {
|
||||
return Object.assign(this.form, this.initial)
|
||||
}
|
||||
let object = this.object
|
||||
if (object === null) {
|
||||
object = await this.getObjectDetail()
|
||||
if (!object) {
|
||||
if (cloneFrom) {
|
||||
this.$log.debug('Clone from: ', cloneFrom)
|
||||
const url = `${this.url}${cloneFrom}/`
|
||||
object = await this.getObjectDetail(url)
|
||||
if (object['name']) {
|
||||
object.name = this.$t('common.cloneFrom') + ' ' + object.name
|
||||
} else if (object['hostname']) {
|
||||
object.hostname = this.$t('common.cloneFrom') + ' ' + object.hostname
|
||||
}
|
||||
} else {
|
||||
object = await this.getObjectDetail(this.iUrl)
|
||||
}
|
||||
}
|
||||
if (object) {
|
||||
object = _.cloneDeep(object)
|
||||
this.$emit('update:object', object)
|
||||
this.$emit('getObjectDone', object)
|
||||
}
|
||||
return object
|
||||
},
|
||||
async getObjectDetail() {
|
||||
return this.$axios.get(this.iUrl)
|
||||
async getObjectDetail(url) {
|
||||
this.$log.debug('Get object detail: ', url)
|
||||
return this.$axios.get(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Page>
|
||||
<Page v-bind="$attrs">
|
||||
<IBox>
|
||||
<GenericCreateUpdateForm ref="createUpdateForm" v-bind="$attrs" v-on="$listeners" />
|
||||
</IBox>
|
||||
|
||||
@@ -19,6 +19,7 @@ import TabPage from '../TabPage'
|
||||
import { flashErrorMsg } from '@/utils/request'
|
||||
import { getApiPath } from '@/utils/common'
|
||||
import ActionsGroup from '@/components/ActionsGroup'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'GenericDetailPage',
|
||||
@@ -80,22 +81,27 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
const defaultActions = {
|
||||
canDelete: true,
|
||||
deleteCallback: function(item) { this.defaultDelete(item) },
|
||||
deleteApiUrl: getApiPath(this),
|
||||
deleteSuccessRoute: this.$route.name.replace('Detail', 'List'),
|
||||
canUpdate: true,
|
||||
canUpdate: () => {
|
||||
return !vm.currentOrgIsRoot
|
||||
},
|
||||
updateCallback: function(item) { this.defaultUpdate(item) },
|
||||
updateRoute: this.$route.name.replace('Detail', 'Update'),
|
||||
detailApiUrl: getApiPath(this)
|
||||
}
|
||||
return {
|
||||
defaultActions: defaultActions,
|
||||
loading: true,
|
||||
validActions: Object.assign(defaultActions, this.actions)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentOrgIsRoot']),
|
||||
pageActions() {
|
||||
return [
|
||||
{
|
||||
@@ -158,7 +164,7 @@ export default {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.$router.push({ name: this.validActions.deleteSuccessRoute })
|
||||
} catch (error) {
|
||||
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
|
||||
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
|
||||
} finally {
|
||||
instance.confirmButtonLoading = false
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<Page v-bind="$attrs">
|
||||
<ListTable ref="ListTable" v-bind="$attrs" />
|
||||
<GenericListTable ref="ListTable" v-bind="$attrs" />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from '@/layout/components/Page'
|
||||
import ListTable from '@/components/ListTable'
|
||||
import GenericListTable from '@/layout/components/GenericListTable'
|
||||
export default {
|
||||
name: 'GenericListPage',
|
||||
components: {
|
||||
Page, ListTable
|
||||
Page, GenericListTable
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
31
src/layout/components/GenericListTable/index.vue
Normal file
31
src/layout/components/GenericListTable/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ListTable ref="ListTable" v-bind="iAttrs" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
name: 'GenericListTable',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentOrgIsRoot']),
|
||||
iAttrs() {
|
||||
const attrs = _.cloneDeep(this.$attrs)
|
||||
const canCreate = _.get(attrs, 'header-actions.canCreate', null)
|
||||
this.$log.debug('Can create: ', canCreate)
|
||||
if (canCreate === null && this.currentOrgIsRoot) {
|
||||
_.set(attrs, 'header-actions.canCreate', false)
|
||||
}
|
||||
this.$log.debug('List table Attrs: ', attrs)
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<Page>
|
||||
<el-alert v-if="helpMessage" type="success"> {{ helpMessage }} </el-alert>
|
||||
<TreeTable ref="TreeTable" :table-config="tableConfig" :header-actions="headerActions" :tree-setting="treeSetting">
|
||||
<TreeTable
|
||||
ref="TreeTable"
|
||||
:table-config="tableConfig"
|
||||
:header-actions="iHeaderActions"
|
||||
:tree-setting="treeSetting"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template #table>
|
||||
<slot name="table" />
|
||||
</template>
|
||||
@@ -15,6 +21,7 @@
|
||||
<script>
|
||||
import Page from '@/layout/components/Page'
|
||||
import TreeTable from '@/components/TreeTable'
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
name: 'GenericTreeListPage',
|
||||
components: {
|
||||
@@ -27,12 +34,30 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentOrg']),
|
||||
iHeaderActions() {
|
||||
const attrs = _.cloneDeep(this.headerActions)
|
||||
const canCreate = _.get(attrs, 'canCreate', null)
|
||||
// this.$log.debug('Current org: ', this.currentOrg)
|
||||
if (canCreate === null && this.currentOrg && this.currentOrg.is_root) {
|
||||
_.set(attrs, 'canCreate', false)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideRMenu() {
|
||||
this.$refs.TreeTable.hideRMenu()
|
||||
},
|
||||
getSelectedNodes: function() {
|
||||
return this.$refs.TreeTable.getSelectedNodes()
|
||||
},
|
||||
getNodes: function() {
|
||||
return this.$refs.TreeTable.getNodes()
|
||||
},
|
||||
selectNode: function(node) {
|
||||
return this.$refs.TreeTable.selectNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="dialogSetting.dialogVisible"
|
||||
:title="this.$t('common.updateSelected')"
|
||||
:visible.sync="dialogSetting.dialogVisible"
|
||||
width="70%"
|
||||
@@ -99,6 +98,7 @@ export default {
|
||||
const url = this.url
|
||||
const msg = this.updateSuccessMsg
|
||||
this.$axios.patch(url, validValues).then((res) => {
|
||||
vm.$emit('update')
|
||||
this.$message.success(msg)
|
||||
vm.dialogSetting.dialogVisible = false
|
||||
}).catch(error => {
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||
<h1 v-else class="sidebar-title">{{ title }}</h1>
|
||||
<img :src="logoSrc" class="sidebar-logo">
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img :src="logoSrc" class="sidebar-logo-text">
|
||||
<!-- <img v-else-if="logoText" :src="logoText" class="sidebar-logo-text">-->
|
||||
<!-- <h1 class="sidebar-title">{{ title }}</h1>-->
|
||||
<img :src="logoTextSrc" class="sidebar-logo-text">
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -26,10 +23,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'JumpServer',
|
||||
logoText: require('@/assets/img/logo-text.png'),
|
||||
logo: require('@/assets/img/logo.png'),
|
||||
xpackData: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -37,16 +30,14 @@ export default {
|
||||
'publicSettings'
|
||||
]),
|
||||
// eslint-disable-next-line vue/return-in-computed-property
|
||||
logoTextSrc() {
|
||||
return this.publicSettings.LOGO_URLS.logo_index
|
||||
},
|
||||
logoSrc() {
|
||||
if (this.publicSettings.LOGO_URLS.logo_index !== '/static/img/logo_text.png') {
|
||||
return this.publicSettings.LOGO_URLS.logo_index
|
||||
} else {
|
||||
return this.logoText
|
||||
}
|
||||
return this.publicSettings.LOGO_URLS.logo_logout
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,7 +48,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
needShow() {
|
||||
return !this.isCollapse && this.userAdminOrgList.length > 1 && this.inAdminPage
|
||||
const otherOrgs = this.userAdminOrgList.filter(org => {
|
||||
return !org.is_root && !org.is_default
|
||||
})
|
||||
return !this.isCollapse && otherOrgs.length > 0 && this.inAdminPage
|
||||
},
|
||||
changeOrg(orgId) {
|
||||
orgUtil.changeOrg(orgId)
|
||||
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
case 'userPage':
|
||||
if (this.currentOrgUsePagePerm) {
|
||||
this.$store.dispatch('users/setCurrentRole', rolec.USER)
|
||||
console.log('Switch to: ', rolec.USER)
|
||||
// console.log('Switch to: ', rolec.USER)
|
||||
window.location.href = `/ui/`
|
||||
}
|
||||
break
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
},
|
||||
logout() {
|
||||
// Clean Status
|
||||
const statusList = ['currentOrg', 'currentRole', 'jms_current_org', 'jms_current_role', 'sidebarStatus', 'django_language', 'X-JMS-ORG', 'activeTab']
|
||||
const statusList = ['currentOrg', 'currentRole', 'jms_current_org', 'jms_current_role', 'sidebarStatus', 'X-JMS-ORG', 'activeTab']
|
||||
for (const i in statusList) {
|
||||
this.$cookie.delete(statusList[i])
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import ListTable from '@/components/ListTable'
|
||||
import { DateFormatter, ShowKeyFormatter } from '@/components/ListTable/formatters'
|
||||
import { DateFormatter, ShowKeyFormatter } from '@/components/TableFormatters'
|
||||
export default {
|
||||
name: 'ApiKey',
|
||||
components: {
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
this.$refs.ListTable.reloadTable()
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
|
||||
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
|
||||
})
|
||||
}.bind(this),
|
||||
extraActions: [
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Language',
|
||||
data() {
|
||||
@@ -31,25 +30,57 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
supportedLangMapper() {
|
||||
return this.supportLanguages.reduce((map, obj) => {
|
||||
map[obj.code] = obj
|
||||
return map
|
||||
})
|
||||
},
|
||||
currentLang() {
|
||||
const cookieCode = this.$cookie.get(this.LANG_COOKIE_NAME)
|
||||
let lang = this.supportLanguages.find((v) => v.cookieCode === cookieCode)
|
||||
const langCode = this.getLangCode()
|
||||
let lang = this.supportedLangMapper[langCode]
|
||||
if (!lang) {
|
||||
lang = this.supportLanguages[0]
|
||||
this.changeLangTo(lang)
|
||||
}
|
||||
if (lang.code !== this.$i18n.locale) {
|
||||
this.changeLangTo(lang)
|
||||
}
|
||||
return lang
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.changeLang()
|
||||
this.changeMomentLang()
|
||||
},
|
||||
methods: {
|
||||
changeLang() {
|
||||
if (this.currentLang.code !== this.$i18n.locale) {
|
||||
this.changeLangTo(this.currentLang)
|
||||
}
|
||||
},
|
||||
changeMomentLang() {
|
||||
if (this.currentLang.code.indexOf('en') > -1) {
|
||||
this.$moment.locale('en')
|
||||
} else {
|
||||
this.$moment.locale('zh-cn')
|
||||
}
|
||||
},
|
||||
changeLangTo(item) {
|
||||
this.$i18n.locale = item.code
|
||||
localStorage.setItem('lang', item.code)
|
||||
this.$cookie.set(this.LANG_COOKIE_NAME, item.cookieCode)
|
||||
window.location.reload()
|
||||
},
|
||||
getLangCode() {
|
||||
let langCode = localStorage.lang
|
||||
if (!langCode) {
|
||||
langCode = this.$cookie.get(this.LANG_COOKIE_NAME)
|
||||
}
|
||||
if (!langCode) {
|
||||
langCode = navigator.language || navigator.userLanguage
|
||||
}
|
||||
langCode = langCode.substr(0, 2)
|
||||
langCode = langCode.replace('zh', 'cn')
|
||||
if (langCode) {
|
||||
return langCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
274
src/layout/components/NavHeader/SiteMessages.vue
Normal file
274
src/layout/components/NavHeader/SiteMessages.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount === 0" :max="99" size="mini" type="primary">
|
||||
<a style="color: #606266 !important; width: 30px" @click="toggleDrawer">
|
||||
<i class="el-icon-message" style="font-size: 18px" />
|
||||
</a>
|
||||
</el-badge>
|
||||
<el-drawer
|
||||
:visible.sync="show"
|
||||
:before-close="handleClose"
|
||||
:modal="false"
|
||||
:title="$t('notifications.SiteMessage')"
|
||||
custom-class="site-msg"
|
||||
size="25%"
|
||||
@open="getMessages"
|
||||
>
|
||||
<div v-if="unreadMsgCount !== 0" class="msg-list">
|
||||
<div
|
||||
v-for="msg of messages"
|
||||
:key="msg.id"
|
||||
class="msg-item"
|
||||
:class="msg.has_read ? 'msg-read' : 'msg-unread'"
|
||||
@mouseover="hoverMsgId = msg.id"
|
||||
@mouseleave="hoverMsgId = ''"
|
||||
@click="showMsgDetail(msg)"
|
||||
>
|
||||
<div class="msg-item-head">
|
||||
<span class="msg-item-head-type">
|
||||
<i :class="msg.has_read ? 'fa-envelope-open-o' : 'fa-envelope'" class="fa msg-icon" />
|
||||
{{ msg.subject }}
|
||||
</span>
|
||||
<span v-if="hoverMsgId !== msg.id || msg.has_read" class="msg-item-head-time">
|
||||
{{ formatDate(msg.date_created) }}
|
||||
</span>
|
||||
<div v-else class="msg-item-read-btn" @click.stop="markAsRead(msg)">
|
||||
<a>{{ $t('notifications.MarkAsRead') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg-item-txt">
|
||||
<span v-html="msg.message" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-msg">
|
||||
{{ $t('notifications.NoUnreadMsg') }}
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<Dialog
|
||||
v-if="msgDetailVisible"
|
||||
:visible.sync="msgDetailVisible"
|
||||
:title="''"
|
||||
:close-on-click-modal="false"
|
||||
:confirm-title="$t('notifications.MarkAsRead')"
|
||||
@confirm="markAsRead(currentMsg)"
|
||||
@cancel="cancelRead"
|
||||
>
|
||||
<div class="msg-detail">
|
||||
<div class="msg-detail-head">
|
||||
<h3>{{ currentMsg.subject }}</h3>
|
||||
<h5>
|
||||
<span class="msg-detail-time">{{ formatDate(currentMsg.date_created) }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="msg-detail-txt">
|
||||
<span v-html="currentMsg.message" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import Dialog from '@/components/Dialog'
|
||||
|
||||
export default {
|
||||
name: 'SiteMessages',
|
||||
components: { Dialog },
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
messages: [],
|
||||
hoverMsgId: '',
|
||||
msgDetailVisible: false,
|
||||
currentMsg: null,
|
||||
unreadMsgCount: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.enablePullMsgCount()
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.show = false
|
||||
},
|
||||
toggleDrawer() {
|
||||
this.show = !this.show
|
||||
},
|
||||
showMsgDetail(msg) {
|
||||
this.currentMsg = msg
|
||||
this.msgDetailVisible = true
|
||||
},
|
||||
getMessages() {
|
||||
const url = '/api/v1/notifications/site-message/?offset=0&limit=15&has_read=false'
|
||||
this.$axios.get(url).then(resp => {
|
||||
this.messages = [...resp.results]
|
||||
this.unreadMsgCount = resp.count
|
||||
})
|
||||
},
|
||||
formatDate(s) {
|
||||
if (!s) {
|
||||
return ''
|
||||
}
|
||||
const d = new Date(s)
|
||||
const now = new Date()
|
||||
if (now.getTime() - d.getTime() > (3600 * 24 * 7) * 1000) {
|
||||
return toSafeLocalDateStr(s)
|
||||
} else {
|
||||
return this.$moment(d).fromNow()
|
||||
}
|
||||
},
|
||||
markAsRead(msg) {
|
||||
const url = `/api/v1/notifications/site-message/mark-as-read/`
|
||||
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
|
||||
this.msgDetailVisible = false
|
||||
this.getMessages()
|
||||
}).catch(err => {
|
||||
this.$message(err.detail)
|
||||
})
|
||||
},
|
||||
cancelRead() {
|
||||
this.msgDetailVisible = false
|
||||
},
|
||||
enablePullMsgCount() {
|
||||
const scheme = document.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const port = document.location.port ? ':' + document.location.port : ''
|
||||
const url = '/ws/notifications/site-msg/'
|
||||
const wsURL = scheme + '://' + document.location.hostname + port + url
|
||||
|
||||
const ws = new WebSocket(wsURL)
|
||||
ws.onopen = (event) => {
|
||||
this.$log.debug('Websocket connected: ', event)
|
||||
}
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
this.$log.debug('Data: ', data)
|
||||
const unreadCount = data['unread_count']
|
||||
if (unreadCount !== undefined) {
|
||||
this.unreadMsgCount = unreadCount
|
||||
}
|
||||
} catch (e) {
|
||||
this.$log.debug('Recv site message error')
|
||||
}
|
||||
}
|
||||
ws.onerror = (error) => {
|
||||
this.$message.error(this.$t('common.ConnectWebSocketError'))
|
||||
this.$log.debug('site message ws error: ', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-badge ::v-deep .el-badge__content.is-fixed{
|
||||
top:10px;
|
||||
}
|
||||
|
||||
.msg-list {
|
||||
padding: 0 25px 20px;
|
||||
}
|
||||
|
||||
>>> .site-msg {
|
||||
.el-drawer__header {
|
||||
border-bottom: solid 1px rgb(231, 234, 239);
|
||||
margin-bottom: 0;
|
||||
padding-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.el-drawer__body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-item {
|
||||
border-bottom: solid 1px rgb(231, 234, 239);
|
||||
padding: 15px 0 10px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
padding: 15px 20px 10px;
|
||||
margin: 0 -20px;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
|
||||
.msg-icon {
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
&.msg-unread {
|
||||
.msg-item-txt {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg-item-head {
|
||||
line-height: 20px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
&:after {
|
||||
clear: both;
|
||||
content: ".";
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg-item-head-type {
|
||||
float: left;
|
||||
width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.msg-item-head-time {
|
||||
float: right;
|
||||
}
|
||||
.msg-item-read-btn {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-item-txt {
|
||||
overflow: hidden;
|
||||
color: #000;
|
||||
padding: 4px 0 0;
|
||||
line-height: 21px;
|
||||
max-height: 21px;
|
||||
display: -webkit-box;
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.msg-detail {
|
||||
padding-left: 20px;
|
||||
|
||||
.msg-detail-time {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.msg-detail-txt {
|
||||
margin-bottom: 20px;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-msg {
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
>>> :focus{ outline:0; }
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<script>
|
||||
import { getTicketOpenCount } from '@/api/ticket'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'WebTerminal',
|
||||
@@ -17,13 +18,17 @@ export default {
|
||||
assignedTicketCount: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'currentUser'
|
||||
])
|
||||
},
|
||||
created() {
|
||||
this.ticketsOpenedCount()
|
||||
},
|
||||
methods: {
|
||||
|
||||
ticketsOpenedCount() {
|
||||
getTicketOpenCount(1).then(data => {
|
||||
getTicketOpenCount(this.currentUser.id).then(data => {
|
||||
this.assignedTicketCount = data.count
|
||||
})
|
||||
},
|
||||
|
||||
@@ -3,23 +3,26 @@
|
||||
<div class="navbar-header">
|
||||
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="header-item">
|
||||
<ul class="navbar-right">
|
||||
<li class="header-item header-icon">
|
||||
<SiteMessages />
|
||||
</li>
|
||||
<li class="header-item" style="margin-left: 10px">
|
||||
<Help />
|
||||
</div>
|
||||
<div class="header-item">
|
||||
</li>
|
||||
<li class="header-item">
|
||||
<Language />
|
||||
</div>
|
||||
<div v-if="publicSettings.TICKETS_ENABLED&&publicSettings.XPACK_LICENSE_IS_VALID" class="header-item">
|
||||
</li>
|
||||
<li v-if="showTickets" class="header-item">
|
||||
<Tickets />
|
||||
</div>
|
||||
<div class="header-item">
|
||||
</li>
|
||||
<li class="header-item">
|
||||
<WebTerminal />
|
||||
</div>
|
||||
<div class="header-item header-profile">
|
||||
</li>
|
||||
<li class="header-item header-profile">
|
||||
<AccountDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,10 +30,12 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import AccountDropdown from './AccountDropdown'
|
||||
import SiteMessages from './SiteMessages'
|
||||
import Help from './Help'
|
||||
import Language from './Language'
|
||||
import WebTerminal from './WebTerminal'
|
||||
import Tickets from './Tickets'
|
||||
import rolc from '@/utils/role'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -40,7 +45,8 @@ export default {
|
||||
Language,
|
||||
Help,
|
||||
Tickets,
|
||||
WebTerminal
|
||||
WebTerminal,
|
||||
SiteMessages
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -48,14 +54,21 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar', 'publicSettings'
|
||||
])
|
||||
'sidebar', 'publicSettings', 'currentOrgRoles'
|
||||
]),
|
||||
isOrgAuditor() {
|
||||
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
|
||||
},
|
||||
showTickets() {
|
||||
return this.publicSettings.TICKETS_ENABLED &&
|
||||
this.publicSettings.XPACK_LICENSE_IS_VALID &&
|
||||
!this.isOrgAuditor
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -80,12 +93,19 @@ export default {
|
||||
.navbar-right {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-item {
|
||||
line-height: 50px;
|
||||
display: inline-block;
|
||||
padding-right: 20px;
|
||||
.header-item {
|
||||
line-height: 50px;
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
@@ -97,5 +117,9 @@ export default {
|
||||
.el-header {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
'currentUser'
|
||||
]),
|
||||
isExpire() {
|
||||
if (this.currentUser.role !== 'Admin') {
|
||||
if (!this.publicSettings.XPACK_ENABLED || this.currentUser.role !== 'Admin') {
|
||||
return false
|
||||
}
|
||||
const intervalDays = this.getIntervalDays(this.licenseData.date_expired)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<slot name="globalNotification">
|
||||
<LicenseExpireTip />
|
||||
<PasswordExpireTip />
|
||||
</slot>
|
||||
<div class="page-heading">
|
||||
<el-row :gutter="0">
|
||||
@@ -21,10 +22,12 @@
|
||||
|
||||
<script>
|
||||
import LicenseExpireTip from '@/layout/components/Page/LicenseExpireTip'
|
||||
import PasswordExpireTip from '@/layout/components/Page/PasswordExpireTip'
|
||||
export default {
|
||||
name: 'PageHeading',
|
||||
components: {
|
||||
LicenseExpireTip
|
||||
LicenseExpireTip,
|
||||
PasswordExpireTip
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
|
||||
55
src/layout/components/Page/PasswordExpireTip.vue
Normal file
55
src/layout/components/Page/PasswordExpireTip.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-alert v-if="isExpire" type="error">
|
||||
{{ isExpire }}
|
||||
</el-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'PasswordExpireTip',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
securityData: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'publicSettings',
|
||||
'currentUser'
|
||||
]),
|
||||
isExpire() {
|
||||
// 用户来源不是Local时不显示密码过期提示
|
||||
if (this.currentUser.source !== 'local') {
|
||||
return false
|
||||
}
|
||||
const intervalTime = this.getIntervalDays(this.currentUser.date_password_last_updated)
|
||||
const securityPasswordExpirationTime = this.publicSettings.SECURITY_PASSWORD_EXPIRATION_TIME
|
||||
if (intervalTime >= securityPasswordExpirationTime) {
|
||||
return this.$t('users.passwordExpired')
|
||||
}
|
||||
if (securityPasswordExpirationTime - intervalTime <= 5) {
|
||||
return this.$t('users.passwordWillExpiredPrefixMsg') + (securityPasswordExpirationTime - intervalTime) + this.$t('users.passwordWillExpiredSuffixMsg')
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getIntervalDays(date) {
|
||||
const dateExpired = new Date(toSafeLocalDateStr(date))
|
||||
const dateNow = new Date()
|
||||
const intervalTime = dateNow.getTime() - dateExpired.getTime()
|
||||
return Math.floor(intervalTime / (24 * 3600 * 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeading>
|
||||
<PageHeading class="disabled-when-print">
|
||||
<slot name="title">{{ iTitle }}</slot>
|
||||
<template #rightSide>
|
||||
<slot name="headingRightSide" />
|
||||
@@ -42,5 +42,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@media print {
|
||||
.disabled-when-print{
|
||||
display: none;
|
||||
}
|
||||
.enabled-when-print{
|
||||
display: inherit !important;
|
||||
}
|
||||
.print-margin{
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div>
|
||||
<el-tabs v-if="submenu.length > 0" slot="submenu" v-model="iActiveMenu" class="page-submenu" @tab-click="handleTabClick">
|
||||
<template v-for="item in submenu">
|
||||
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name">
|
||||
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name" :disabled="item.disabled">
|
||||
<span slot="label">
|
||||
{{ item.title }}
|
||||
<slot name="badge" :tab="item.name" />
|
||||
@@ -80,22 +80,23 @@ export default {
|
||||
},
|
||||
getPropActiveTab() {
|
||||
let activeTab = ''
|
||||
let tabObj = null
|
||||
|
||||
const activeTabs = [
|
||||
const preActiveTabs = [
|
||||
this.$route.query[ACTIVE_TAB_KEY],
|
||||
this.$cookie.get(ACTIVE_TAB_KEY),
|
||||
this.activeMenu
|
||||
]
|
||||
|
||||
for (activeTab of activeTabs) {
|
||||
tabObj = this.tabIndices[activeTab]
|
||||
if (tabObj !== undefined) {
|
||||
return activeTab
|
||||
for (const preTab of preActiveTabs) {
|
||||
for (const tabName in this.tabIndices) {
|
||||
if (preTab && tabName && preTab.toLowerCase() === tabName.toLowerCase()) {
|
||||
return tabName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.submenu[0].name
|
||||
activeTab = this.submenu[0].name
|
||||
return activeTab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="device==='mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<NavBar class="sidebar-container" />
|
||||
<NavBar class="sidebar-container disabled-when-print" />
|
||||
<div :class="{hasTagsView:needTagsView}" class="main-container">
|
||||
<div :class="{'fixed-header':fixedHeader}">
|
||||
<div :class="{'fixed-header':fixedHeader}" class="disabled-when-print">
|
||||
<NavHeader />
|
||||
<tags-view v-if="needTagsView" />
|
||||
</div>
|
||||
<app-main />
|
||||
<Footer />
|
||||
<Footer class="disabled-when-print" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -98,4 +98,28 @@ export default {
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
@media print {
|
||||
.disabled-when-print{
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
.enabled-when-print{
|
||||
display: inherit !important;
|
||||
}
|
||||
.print-margin{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.drawer-bg{
|
||||
display: none;
|
||||
}
|
||||
.main-container{
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
//.fixed-header{
|
||||
// width: 100% !important;
|
||||
//}
|
||||
//.hideSidebar .fixed-header{
|
||||
// width: 100% !important;
|
||||
//}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,8 +40,13 @@ Vue.config.productionTip = false
|
||||
import VueCookie from 'vue-cookie'
|
||||
Vue.use(VueCookie)
|
||||
window.$cookie = VueCookie
|
||||
import VueMoment from 'vue-moment'
|
||||
Vue.use(VueMoment)
|
||||
|
||||
const moment = require('moment')
|
||||
require('moment/locale/zh-cn')
|
||||
Vue.use(require('vue-moment'), {
|
||||
moment
|
||||
})
|
||||
|
||||
// logger
|
||||
import VueLogger from 'vuejs-logger'
|
||||
import loggerOptions from './utils/logger'
|
||||
|
||||
120
src/router/accounts.js
Normal file
120
src/router/accounts.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import empty from '@/layout/empty'
|
||||
export default [
|
||||
{
|
||||
path: 'asset-accounts',
|
||||
component: empty,
|
||||
meta: { title: i18n.t('route.AssetAccount') },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AssetAccountList',
|
||||
component: () => import('@/views/accounts/AssetAccount/AssetAccountList'),
|
||||
meta: { title: i18n.t('route.AssetAccount') }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'application-accounts',
|
||||
component: empty,
|
||||
meta: { title: i18n.t('route.AssetAccount') },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'ApplicationAccountList',
|
||||
component: () => import('@/views/accounts/ApplicationAccount/ApplicationAccountList'),
|
||||
meta: { title: i18n.t('route.ApplicationAccount') }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'gathered-user',
|
||||
component: empty,
|
||||
redirect: '',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/accounts/GatheredUser/index'),
|
||||
name: 'GatherUserListIndex',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/accounts/gathered-user' }
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/accounts/GatheredUser/GatheredUserList'),
|
||||
name: 'GatherUserList',
|
||||
hidden: true,
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/accounts/gathered-user' }
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: () => import('@/views/accounts/GatheredUser/TaskList'),
|
||||
name: 'GatherUserTaskList',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/accounts/gathered-user' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'tasks/:id',
|
||||
component: () => import('@/views/accounts/GatheredUser/TaskDetail/index'),
|
||||
name: 'GatherUserTaskDetail',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/accounts/gathered-user' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'tasks/create',
|
||||
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
|
||||
name: 'GatherUserTaskCreate',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/accounts/gathered-user' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'tasks/:id/update',
|
||||
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
|
||||
name: 'GatherUserTaskUpdate',
|
||||
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/accounts/gathered-user' },
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'change-auth-plan',
|
||||
component: empty,
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
|
||||
children: [
|
||||
{
|
||||
path: 'plan',
|
||||
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
|
||||
name: 'ChangeAuthPlanList',
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
|
||||
},
|
||||
{
|
||||
path: 'plan/create',
|
||||
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
|
||||
name: 'ChangeAuthPlanCreate',
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'create' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'plan/:id/update',
|
||||
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
|
||||
name: 'ChangeAuthPlanUpdate',
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'update' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'plan/:id',
|
||||
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
|
||||
name: 'ChangeAuthPlanDetail',
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'plan-execution/:id',
|
||||
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
|
||||
name: 'ChangeAuthPlanExecutionDetail',
|
||||
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
42
src/router/acl.js
Normal file
42
src/router/acl.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import empty from '@/layout/empty'
|
||||
export default [
|
||||
{
|
||||
path: 'asset-acl',
|
||||
component: empty,
|
||||
redirect: '',
|
||||
meta: {
|
||||
title: i18n.t('route.AssetAclList'),
|
||||
licenseRequired: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AssetAclList',
|
||||
component: () => import('@/views/acl/AssetAcl/AssetAclList'),
|
||||
meta: { title: i18n.t('route.AssetAclList'), activeMenu: '/acl/asset-acl' }
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'AssetAclCreate',
|
||||
component: () => import('@/views/acl/AssetAcl/AssetAclCreateUpdate'),
|
||||
meta: { title: i18n.t('route.AssetAclCreate'), activeMenu: '/acl/asset-acl' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
name: 'AssetAclDetail',
|
||||
component: () => import('@/views/acl/AssetAcl/AssetAclDetail'),
|
||||
meta: { title: i18n.t('route.AssetAclDetail'), activeMenu: '/acl/asset-acl' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: ':id/update',
|
||||
name: 'AssetAclUpdate',
|
||||
component: () => import('@/views/acl/AssetAcl/AssetAclCreateUpdate'),
|
||||
meta: { title: i18n.t('route.AssetAclUpdate'), activeMenu: '/acl/asset-acl' },
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -34,6 +34,13 @@ export default [
|
||||
component: () => import('@/views/assets/Asset/AssetCreateUpdate.vue'),
|
||||
meta: { title: i18n.t('route.AssetUpdate'), activeMenu: '/assets/assets' },
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: 'detail/:id/update',
|
||||
name: 'AssetMoreInformationEdit',
|
||||
component: () => import('@/views/assets/Asset/AssetMoreInformationEdit.vue'),
|
||||
meta: { title: i18n.t('common.UpdateAssetDetail'), activeMenu: '/assets/assets' },
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -45,5 +45,12 @@ export default [
|
||||
name: 'CeleryTaskLog',
|
||||
hidden: true,
|
||||
meta: { title: i18n.t('route.CeleryTaskLog'), roles: ['SuperAdmin', 'Admin', 'Auditor', 'User'] }
|
||||
},
|
||||
{
|
||||
path: '/ops/task/task/:id/log/',
|
||||
component: () => import('@/views/ops/CeleryTaskLog'),
|
||||
name: 'TaskLog',
|
||||
hidden: true,
|
||||
meta: { title: i18n.t('route.CeleryTaskLog'), roles: ['SuperAdmin', 'Admin', 'Auditor', 'User'] }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,8 @@ import OpsRoutes from './ops'
|
||||
import TicketsRoutes from './tickets'
|
||||
import AuditsRoutes from './audits'
|
||||
import commonRoutes from './common'
|
||||
import aclRoutes from './acl'
|
||||
import AccountRoutes from './accounts'
|
||||
|
||||
/**
|
||||
* constantRoutes
|
||||
@@ -109,6 +111,18 @@ export const allRoleRoutes = [
|
||||
meta: { title: i18n.t('route.Applications'), icon: 'th' },
|
||||
children: ApplicationsRoute
|
||||
},
|
||||
{
|
||||
path: '/accounts',
|
||||
component: Layout,
|
||||
redirect: '/accounts/asset-accounts/',
|
||||
name: 'Accounts',
|
||||
meta: {
|
||||
licenseRequired: true,
|
||||
title: i18n.t('route.Accounts'),
|
||||
icon: 'address-book'
|
||||
},
|
||||
children: AccountRoutes
|
||||
},
|
||||
{
|
||||
path: '/perms/',
|
||||
component: Layout,
|
||||
@@ -117,6 +131,17 @@ export const allRoleRoutes = [
|
||||
meta: { title: i18n.t('route.Perms'), icon: 'edit' },
|
||||
children: PermsRoute
|
||||
},
|
||||
{
|
||||
path: '/acl/',
|
||||
component: Layout,
|
||||
redirect: '/perms/access-control-list/',
|
||||
name: 'Acl',
|
||||
meta: {
|
||||
licenseRequired: true,
|
||||
title: i18n.t('route.Acl'),
|
||||
icon: 'fort-awesome' },
|
||||
children: aclRoutes
|
||||
},
|
||||
{
|
||||
path: '/terminal/',
|
||||
component: Layout,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user