mirror of
https://github.com/jumpserver/lina.git
synced 2025-11-12 21:34:02 +00:00
Compare commits
866 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e8de4c73b | ||
|
|
47a0641fed | ||
|
|
ad118c5eda | ||
|
|
9bd3b37371 | ||
|
|
274f50da95 | ||
|
|
8f87c42c8c | ||
|
|
35c793785c | ||
|
|
abe176831b | ||
|
|
c97512c99b | ||
|
|
6a8553ddfd | ||
|
|
23f1f371e6 | ||
|
|
0878e93f24 | ||
|
|
cc76a66a34 | ||
|
|
d17556cdc7 | ||
|
|
f5f4edaf63 | ||
|
|
62329d57a4 | ||
|
|
bc90d1e9c6 | ||
|
|
60d4da29cb | ||
|
|
b9bdf35bb9 | ||
|
|
b9b6bccb7f | ||
|
|
f2bbe4841a | ||
|
|
d920eeda7a | ||
|
|
7361acae6a | ||
|
|
a56799ca22 | ||
|
|
88ddd86154 | ||
|
|
d9532d5a93 | ||
|
|
02b9e83e3d | ||
|
|
a29afbbeea | ||
|
|
0f4f4a2ca7 | ||
|
|
a23cd151ae | ||
|
|
5f3855d8c1 | ||
|
|
6ad44e2830 | ||
|
|
51446c0c9a | ||
|
|
e3a493a6c5 | ||
|
|
9dfca98828 | ||
|
|
599a1bb705 | ||
|
|
52e4a6d6ed | ||
|
|
4b5ac9aaa9 | ||
|
|
3b2c4975b6 | ||
|
|
c3cbafce6d | ||
|
|
b75f0be9e5 | ||
|
|
8df0f05ee5 | ||
|
|
fa7a78523f | ||
|
|
11e803be19 | ||
|
|
060781443f | ||
|
|
555c8fd2d6 | ||
|
|
2c84e4ffa6 | ||
|
|
16dafaaf42 | ||
|
|
819438a2e8 | ||
|
|
61e8467849 | ||
|
|
491f3d3dde | ||
|
|
4588965a8d | ||
|
|
48d07b4d6d | ||
|
|
45c74ba470 | ||
|
|
2e2c8fe086 | ||
|
|
459843669b | ||
|
|
f839e1fc0d | ||
|
|
399b381aa9 | ||
|
|
7670c277aa | ||
|
|
cbc78de7c4 | ||
|
|
e64cd82dba | ||
|
|
f154a252d3 | ||
|
|
984483c470 | ||
|
|
e8f013a5be | ||
|
|
e1850d758f | ||
|
|
60e5c994f4 | ||
|
|
1adf138175 | ||
|
|
4e5ed82ec7 | ||
|
|
52e2d58567 | ||
|
|
411fca98b0 | ||
|
|
e0c3243986 | ||
|
|
e4240d7a8f | ||
|
|
db4b09603b | ||
|
|
a88499d10c | ||
|
|
0b2cfb88a2 | ||
|
|
13443a7137 | ||
|
|
5aa3226132 | ||
|
|
ea9316f0a0 | ||
|
|
d05d3f3fa3 | ||
|
|
5da000c8a1 | ||
|
|
f85d3567a7 | ||
|
|
78174b23fd | ||
|
|
07151b2bfe | ||
|
|
1cc395c22d | ||
|
|
a18507f9c6 | ||
|
|
790e2315e2 | ||
|
|
4f2f4a7a9d | ||
|
|
32817390c2 | ||
|
|
8af7c46f02 | ||
|
|
ed4162cbd5 | ||
|
|
973b1fbae2 | ||
|
|
8906fa3292 | ||
|
|
5c94c1c870 | ||
|
|
16757b7df0 | ||
|
|
679103db03 | ||
|
|
a7aa763e37 | ||
|
|
f30ba0a98d | ||
|
|
c426841a7c | ||
|
|
288bfe4727 | ||
|
|
c4c39188aa | ||
|
|
52d8bfeac3 | ||
|
|
20c201f351 | ||
|
|
94b5aa700d | ||
|
|
e030fc313e | ||
|
|
917fa0ea44 | ||
|
|
68df311a9c | ||
|
|
e819e4aaff | ||
|
|
364b6b0bcf | ||
|
|
a0534e0c2e | ||
|
|
0152ad269b | ||
|
|
b651f82eb6 | ||
|
|
0e665912ff | ||
|
|
8929957fd9 | ||
|
|
959bb56a3e | ||
|
|
cb5fe0f2a0 | ||
|
|
05c06e4fc2 | ||
|
|
d3ae9e3f0f | ||
|
|
1af14a5743 | ||
|
|
7c0621b357 | ||
|
|
4d4085798d | ||
|
|
0da0c2af4f | ||
|
|
ba78bdeed7 | ||
|
|
0f76f6ad96 | ||
|
|
2a14b0cfcd | ||
|
|
8537a7b6e4 | ||
|
|
bf74adc037 | ||
|
|
f7d75ca92c | ||
|
|
0d3029e6b1 | ||
|
|
7fda66cda4 | ||
|
|
de4af07858 | ||
|
|
6bab31d74c | ||
|
|
1c6da15301 | ||
|
|
cbd18814f6 | ||
|
|
3be8794a58 | ||
|
|
2183cf9bb0 | ||
|
|
dfd710daf7 | ||
|
|
e38817b24a | ||
|
|
34b18d5220 | ||
|
|
d07b311b76 | ||
|
|
0e67594ba9 | ||
|
|
5f3b307785 | ||
|
|
96ee47b4ac | ||
|
|
574ec53d23 | ||
|
|
1720c183ad | ||
|
|
9bbd2ac293 | ||
|
|
fc7bb98fbb | ||
|
|
32d37a4860 | ||
|
|
699b12178a | ||
|
|
fb87ebd0e7 | ||
|
|
b8ba53cf5e | ||
|
|
c3f27dd0a8 | ||
|
|
c94544df39 | ||
|
|
35c7b81359 | ||
|
|
e7ae4bfbd3 | ||
|
|
d1c3c63efc | ||
|
|
bfc5ae9bf7 | ||
|
|
fa0c4d66bb | ||
|
|
31d011b1fc | ||
|
|
f4cd0efd29 | ||
|
|
f946cf5d11 | ||
|
|
8d42071d4a | ||
|
|
b374979b32 | ||
|
|
430f2e4856 | ||
|
|
59a8971af0 | ||
|
|
4f70f3bea7 | ||
|
|
30ffa13b01 | ||
|
|
4fb8e4073c | ||
|
|
fef38ea007 | ||
|
|
41c5ac5f12 | ||
|
|
4056c601b1 | ||
|
|
34a72467d2 | ||
|
|
e4657f3bf6 | ||
|
|
c6cde723ec | ||
|
|
8b31666f67 | ||
|
|
0a01ad3552 | ||
|
|
8d783dd650 | ||
|
|
425e47d613 | ||
|
|
2bf83e241e | ||
|
|
ec3bdb0eac | ||
|
|
49ee321c83 | ||
|
|
44f93f5aea | ||
|
|
baf6ba7496 | ||
|
|
fa9b4abba1 | ||
|
|
35e0d57a05 | ||
|
|
90eebea2e6 | ||
|
|
dab32e33ad | ||
|
|
bf877f415a | ||
|
|
1ffb1d9cab | ||
|
|
4d4dc35735 | ||
|
|
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 | ||
|
|
16476caa1e | ||
|
|
6a5c28ac26 | ||
|
|
5335faa789 | ||
|
|
fd1ee6ef7d | ||
|
|
52d8c34bbf | ||
|
|
9eac41c0c3 | ||
|
|
6881316203 | ||
|
|
d4ee8379e8 | ||
|
|
f64e877491 | ||
|
|
f46a63cfcf | ||
|
|
65a71df10e | ||
|
|
fd6b0532ba | ||
|
|
e02d05a327 | ||
|
|
23740cdce0 | ||
|
|
a50f224227 | ||
|
|
f8ec327f11 | ||
|
|
1d76e037a4 | ||
|
|
98f5f38694 | ||
|
|
b78b95e67a | ||
|
|
5f60130952 | ||
|
|
5a82931fc2 | ||
|
|
ad49e3250b | ||
|
|
7ef95f4567 | ||
|
|
25a9d21fd7 | ||
|
|
b849df1dc1 | ||
|
|
1519ccb8e2 | ||
|
|
47e88e7bb4 | ||
|
|
c86aef999c | ||
|
|
2f9d7ab826 | ||
|
|
b4311b8a59 | ||
|
|
04a97a9923 | ||
|
|
c7624f9092 | ||
|
|
fc29fc6c6d | ||
|
|
7afc501db5 | ||
|
|
8ed5672e95 | ||
|
|
951c9f56c5 | ||
|
|
2c0e079aa2 | ||
|
|
a8e7ea9c80 | ||
|
|
30143f833a | ||
|
|
ad88daef9a | ||
|
|
9eb80eb6ca | ||
|
|
112de6e81c | ||
|
|
d01f903a9e | ||
|
|
af6d0aff7c | ||
|
|
edf8621e8f | ||
|
|
eeb15c624a | ||
|
|
79e2d49a3d | ||
|
|
628395e447 | ||
|
|
6cb6e6444b | ||
|
|
1b9aa23761 | ||
|
|
537c385ecf | ||
|
|
c0f7d6e7ff | ||
|
|
78df96f888 | ||
|
|
829957090d | ||
|
|
026c8f37ea | ||
|
|
97340c6aac | ||
|
|
bbe54eae48 | ||
|
|
9137207055 | ||
|
|
0ef30ef651 | ||
|
|
5ae8a6c9e4 | ||
|
|
ace1dcd0b8 | ||
|
|
5df487d6bd | ||
|
|
6e548749e1 | ||
|
|
99885d9f28 | ||
|
|
83163e11e3 | ||
|
|
8c191fee67 | ||
|
|
ff9862fa06 | ||
|
|
db0cea7051 | ||
|
|
305c713a57 | ||
|
|
8a5f93e268 | ||
|
|
c4d262150b | ||
|
|
94f161f7e6 | ||
|
|
2c5bfb3f4c | ||
|
|
8e12837a77 | ||
|
|
4385d84f01 | ||
|
|
214bb28c4c | ||
|
|
14c2285ac8 | ||
|
|
18cbe578f0 | ||
|
|
e19ded8365 | ||
|
|
0150008075 | ||
|
|
b0bca65cab | ||
|
|
cb95b9ba4f | ||
|
|
4a840288c5 | ||
|
|
11b1d6638d | ||
|
|
2225807a36 | ||
|
|
a8baca81d5 | ||
|
|
30161c7178 | ||
|
|
e222d147f6 | ||
|
|
3ddc41707c | ||
|
|
a245055150 | ||
|
|
ba5fdf2027 | ||
|
|
e1999e5ce8 | ||
|
|
be4e0b5e35 | ||
|
|
82c381b80d | ||
|
|
7d71aa96b9 | ||
|
|
93408e52c1 | ||
|
|
cea03df4eb | ||
|
|
72ee5f60b9 | ||
|
|
f6a8e5634b | ||
|
|
868e77c983 | ||
|
|
2a3fd42ca1 | ||
|
|
596a26bfb6 | ||
|
|
2fc8cea9ef | ||
|
|
6f8a5c2bfc | ||
|
|
260901351f | ||
|
|
66845e58db | ||
|
|
0e9e549bea | ||
|
|
785611414e | ||
|
|
19267ee001 | ||
|
|
65326916ca | ||
|
|
0b925ccf33 | ||
|
|
58505d2b50 | ||
|
|
ecae504a80 | ||
|
|
844bc5b44f | ||
|
|
c1dcf82fbd | ||
|
|
336406ddff | ||
|
|
81e8e650bf | ||
|
|
af6308e1b3 | ||
|
|
aae552f374 | ||
|
|
b4c1ee786a | ||
|
|
61da88114d | ||
|
|
279859ce81 | ||
|
|
820bb075a3 | ||
|
|
d96bd76ca9 | ||
|
|
7c56c889f2 | ||
|
|
3547fb26ad | ||
|
|
ac1363b377 | ||
|
|
6b5c90ee86 | ||
|
|
8164fa57ef | ||
|
|
96c9f229e2 | ||
|
|
58313f5fe0 | ||
|
|
738a9c3da1 | ||
|
|
0f10ed9ffc | ||
|
|
294e05cb06 | ||
|
|
1598dcbfbc | ||
|
|
cf2d6a47c2 | ||
|
|
c8fb334dae | ||
|
|
93dba0bbee | ||
|
|
2832d876fd | ||
|
|
5cd89cee6a | ||
|
|
2f69861361 | ||
|
|
8f51d9b0ea | ||
|
|
3a2b6d79fb | ||
|
|
67ede69685 | ||
|
|
17c4c9b2ef | ||
|
|
0319d43942 | ||
|
|
dcd088fd58 | ||
|
|
9875ded710 | ||
|
|
e26cd95ef9 | ||
|
|
1bb8e8c709 | ||
|
|
09617fa606 | ||
|
|
62a6d11332 | ||
|
|
8f00dbf23e | ||
|
|
8fd624e0b7 | ||
|
|
cef6521a2b | ||
|
|
da1217972a | ||
|
|
9efacb68b6 | ||
|
|
b3c22f96d8 | ||
|
|
6bf15655b7 | ||
|
|
ee3dc30985 | ||
|
|
21da017f8e | ||
|
|
38b4810d9e | ||
|
|
d49aae69ab | ||
|
|
012fefa3ea | ||
|
|
1f91b9a72f | ||
|
|
e061d9eb75 | ||
|
|
00cd04e103 | ||
|
|
4b3b1a723f | ||
|
|
2d3a43c202 | ||
|
|
e8e751668d | ||
|
|
c6e0a17aaa | ||
|
|
066d81446c | ||
|
|
a63b07cf2e | ||
|
|
0ab96fa413 | ||
|
|
e064c1cfc4 | ||
|
|
2913699597 | ||
|
|
c413623a22 | ||
|
|
92f08605df | ||
|
|
3fb32ad81c | ||
|
|
760dbad5ac | ||
|
|
878933bc07 | ||
|
|
603ebff771 | ||
|
|
c23ed70df4 | ||
|
|
d21598cf1c | ||
|
|
34f1b5d662 | ||
|
|
c493aca11b | ||
|
|
cc937d600b | ||
|
|
fcbe61cb92 | ||
|
|
b39f01a023 | ||
|
|
794ad35f84 | ||
|
|
729b07798e | ||
|
|
f2514f68b8 | ||
|
|
30d4044d20 | ||
|
|
e7cd6e49e8 | ||
|
|
75276d37e4 | ||
|
|
5a92c4f3ee | ||
|
|
ae6fb22fae | ||
|
|
ccd98606a1 | ||
|
|
7f2ed5d038 | ||
|
|
d24741ab4b | ||
|
|
9ac5eabff2 | ||
|
|
18b5fafa41 | ||
|
|
8a76bb05ac | ||
|
|
ad5b5c7c20 | ||
|
|
fe7d43f669 | ||
|
|
aad23f3de5 | ||
|
|
9c33093b01 | ||
|
|
c94a5dfa1f | ||
|
|
143973531c | ||
|
|
7db590950c | ||
|
|
7e65a52062 | ||
|
|
226d118d28 | ||
|
|
0e3bd186fe | ||
|
|
50e8dfc86d | ||
|
|
a023e03074 | ||
|
|
a0a592f064 | ||
|
|
4d86edd65e | ||
|
|
c597bc1dca | ||
|
|
a3d45fd4b9 | ||
|
|
e16d3c2e3f | ||
|
|
e251127f6e | ||
|
|
58b8917739 | ||
|
|
ad423e921a | ||
|
|
326302ea5f | ||
|
|
be213268fa | ||
|
|
b82231f3ea | ||
|
|
f976800cde | ||
|
|
4973c62618 | ||
|
|
5db7538216 | ||
|
|
786528f9b2 | ||
|
|
661409b99d | ||
|
|
702bb3acfa | ||
|
|
448b64757c | ||
|
|
4289c36bd4 | ||
|
|
bbccba3731 | ||
|
|
57b6e02960 | ||
|
|
8c6d2a1150 | ||
|
|
1295fb7fd2 | ||
|
|
d3fbb9a391 | ||
|
|
2b14cf0225 | ||
|
|
a78f8a3633 | ||
|
|
c4868dabac | ||
|
|
faf848dca5 | ||
|
|
05edffe173 | ||
|
|
4b3862443a | ||
|
|
536ebd7513 | ||
|
|
7dce39d79c | ||
|
|
a8389304d6 | ||
|
|
649d4ac848 | ||
|
|
781bbe0ffa | ||
|
|
2771d80749 | ||
|
|
86b5cb81fc | ||
|
|
1ffcf9e7b4 | ||
|
|
a665d6ed20 | ||
|
|
47c05922ae | ||
|
|
7db080b418 | ||
|
|
57e4b65059 | ||
|
|
5aa28903dd |
@@ -1,8 +1,6 @@
|
||||
# 全局环境变量 请勿随意改动
|
||||
ENV = 'development'
|
||||
|
||||
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = ''
|
||||
VUE_APP_PUBLIC_PATH = '/ui/'
|
||||
@@ -23,4 +21,5 @@ VUE_APP_LOGOUT_PATH = '/core/auth/logout/'
|
||||
|
||||
# Dev server for core proxy
|
||||
VUE_APP_CORE_HOST = 'http://localhost:8080'
|
||||
VUE_APP_CORE_WS = 'ws://localhost:8080'
|
||||
VUE_APP_ENV = 'development'
|
||||
|
||||
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
12
.github/workflows/jms-generic-action-handler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
on: [push, pull_request, release]
|
||||
|
||||
name: JumpServer repos generic handler
|
||||
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: Run generic handler
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "src/views/xpack"]
|
||||
path = src/views/xpack
|
||||
url = git@github.com:jumpserver/lina-xpack.git
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,12 +1,23 @@
|
||||
FROM node:10 as stage-build
|
||||
WORKDIR /data
|
||||
ADD ./package.json /data/package.json
|
||||
ADD ./yarn.lock /data/yarn.lock
|
||||
RUN yarn
|
||||
ADD . /data
|
||||
RUN yarn build:prod
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
ARG NPM_REGISTRY="https://registry.npm.taobao.org"
|
||||
ENV NPM_REGISTY=$NPM_REGISTRY
|
||||
ARG SASS_BINARY_SITE="https://npm.taobao.org/mirrors/node-sass"
|
||||
ENV SASS_BINARY_SITE=$SASS_BINARY_SITE
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
RUN npm config set sass_binary_site=${SASS_BINARY_SITE}
|
||||
RUN npm config set registry ${NPM_REGISTRY}
|
||||
RUN yarn config set registry ${NPM_REGISTRY}
|
||||
COPY package.json yarn.lock /data/
|
||||
RUN yarn install
|
||||
RUN npm rebuild node-sass
|
||||
|
||||
ADD . /data
|
||||
RUN cd utils && bash -xieu build.sh build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=stage-build /data/lina /opt/lina/
|
||||
COPY --from=stage-build /data/release/lina /opt/lina
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
}
|
||||
|
||||
14
nginx.conf
14
nginx.conf
@@ -1,9 +1,19 @@
|
||||
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/;
|
||||
try_files $uri / /ui/index.html;
|
||||
alias /opt/lina/;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
15
package.json
15
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",
|
||||
@@ -82,8 +91,8 @@
|
||||
"less-loader": "^5.0.0",
|
||||
"lint-staged": "^10.1.2",
|
||||
"mockjs": "1.0.1-beta3",
|
||||
"node-sass": "^4.9.0",
|
||||
"runjs": "^4.3.2",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"script-loader": "0.7.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -3,8 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<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'
|
||||
})
|
||||
}
|
||||
@@ -57,6 +57,26 @@ export function TestReplayStorage(id) {
|
||||
})
|
||||
}
|
||||
|
||||
function SetToDefaultStorage(url) {
|
||||
return request({
|
||||
url: url,
|
||||
method: 'patch',
|
||||
data: { 'is_default': true }
|
||||
})
|
||||
}
|
||||
|
||||
export function SetToDefaultCommandStorage(id) {
|
||||
return SetToDefaultStorage(
|
||||
`/api/v1/terminal/command-storages/${id}/`,
|
||||
)
|
||||
}
|
||||
|
||||
export function SetToDefaultReplayStorage(id) {
|
||||
return SetToDefaultStorage(
|
||||
`/api/v1/terminal/replay-storages/${id}/`,
|
||||
)
|
||||
}
|
||||
|
||||
export function getReplayStorage(id) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/replay-storages/${id}/`,
|
||||
|
||||
@@ -51,7 +51,7 @@ export function refreshLdapUserCache() {
|
||||
})
|
||||
}
|
||||
|
||||
export function StartLdapUserCache() {
|
||||
export function startLdapUserCache() {
|
||||
return request({
|
||||
disableFlashErrorMsg: true,
|
||||
url: '/api/v1/settings/ldap/users/?cache_police=1',
|
||||
|
||||
8
src/api/ticket.js
Normal file
8
src/api/ticket.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getTicketOpenCount(assign) {
|
||||
return request({
|
||||
url: `/api/v1/tickets/tickets/?assignees__id=${assign}&status=open&offset=0&limit=15&display=1&draw=1/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -65,3 +65,7 @@ export function logout() {
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshSessionIdAge() {
|
||||
return getProfile()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
83
src/components/AccountListTable/ShowSecretInfo.vue
Normal file
83
src/components/AccountListTable/ShowSecretInfo.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<MFAVerifyDialog
|
||||
@MFAVerifyDone="getAuthInfo"
|
||||
@MFAVerifyCancel="exit"
|
||||
/>
|
||||
<Dialog
|
||||
:title="dialogTitle"
|
||||
:show-confirm="false"
|
||||
:show-cancel="false"
|
||||
:destroy-on-close="true"
|
||||
:width="'50'"
|
||||
:visible.sync="showAuthInfo"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div>
|
||||
<el-form label-position="right" label-width="80px" :model="authInfo">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="account.hostname" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="authInfo.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.SSHKey')">
|
||||
<el-input v-model="authInfo['private_key']" type="password" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
MFAVerifyDialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogTitle: this.$t('common.ViewSecret'),
|
||||
authInfo: {},
|
||||
showAuthInfo: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAuthInfo()
|
||||
},
|
||||
methods: {
|
||||
getAuthInfo() {
|
||||
const url = `/api/v1/assets/account-secrets/${this.account.id}/`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.authInfo = resp
|
||||
this.showAuthInfo = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
92
src/components/AccountListTable/UpdateSecretInfo.vue
Normal file
92
src/components/AccountListTable/UpdateSecretInfo.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<Dialog
|
||||
width="50"
|
||||
:title="this.$t('assets.UpdateAssetUserToken')"
|
||||
:destroy-on-close="true"
|
||||
v-bind="$attrs"
|
||||
@confirm="handleConfirm()"
|
||||
@cancel="handleCancel()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form label-position="right" label-width="80px">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="account.hostname" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="authInfo.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.SSHKey')">
|
||||
<input type="file" @change="onPrivateKeyLoaded">
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
export default {
|
||||
name: 'UpdateSecretInfo',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
authInfo: {
|
||||
password: '',
|
||||
private_key: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleConfirm() {
|
||||
const data = {}
|
||||
if (this.authInfo.password !== '') {
|
||||
data.password = this.authInfo.password
|
||||
}
|
||||
if (this.authInfo.private_key !== '') {
|
||||
data.private_key = this.authInfo.private_key
|
||||
}
|
||||
this.$axios.patch(
|
||||
`/api/v1/assets/accounts/${this.account.id}/`,
|
||||
data
|
||||
).then(res => {
|
||||
this.authInfo = { password: '', private_key: '' }
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
this.$emit('updateAuthDone', res)
|
||||
this.$emit('update:visible', false)
|
||||
}).catch(err => {
|
||||
const errMsg = Object.values(err.response.data).join(', ')
|
||||
this.$message.error(this.$tc('common.updateErrorMsg') + ' ' + errMsg)
|
||||
this.$emit('update:visible', true)
|
||||
})
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
onPrivateKeyLoaded(e) {
|
||||
const vm = this
|
||||
// TODO 校验文件类型
|
||||
const reader = new FileReader()
|
||||
reader.onload = function() {
|
||||
vm.authInfo.private_key = this.result
|
||||
}
|
||||
reader.readAsText(
|
||||
e.target.files[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
31
src/components/AccountListTable/const.js
Normal file
31
src/components/AccountListTable/const.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ChoicesFormatter } from '@/components/TableFormatters'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Reachable'),
|
||||
formatter: ChoicesFormatter,
|
||||
formatterArgs: {
|
||||
iconChoices: {
|
||||
ok: 'fa-check text-primary',
|
||||
failed: 'fa-times text-danger',
|
||||
unknown: 'fa-circle text-warning'
|
||||
},
|
||||
hasTips: true,
|
||||
getTips: ({ row, cellValue }) => {
|
||||
const mapper = {
|
||||
'ok': i18n.t('assets.Reachable'),
|
||||
'failed': i18n.t('assets.Unreachable'),
|
||||
'unknown': i18n.t('assets.Unknown')
|
||||
}
|
||||
let tips = mapper[cellValue]
|
||||
if (row['date_verified']) {
|
||||
const datetime = toSafeLocalDateStr(row['date_verified'])
|
||||
tips += '<br> ' + datetime
|
||||
}
|
||||
return tips
|
||||
}
|
||||
},
|
||||
width: '90px',
|
||||
align: 'center'
|
||||
}
|
||||
184
src/components/AccountListTable/index.vue
Normal file
184
src/components/AccountListTable/index.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
|
||||
<UpdateSecretInfo :visible.sync="showUpdateSecretDialog" :account="account" @updateAuthDone="onUpdateAuthDone" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
|
||||
import ShowSecretInfo from './ShowSecretInfo'
|
||||
import UpdateSecretInfo from './UpdateSecretInfo'
|
||||
import { connectivityMeta } from './const'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'AccountListTable',
|
||||
components: {
|
||||
ListTable,
|
||||
UpdateSecretInfo,
|
||||
ShowSecretInfo
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
exportUrl: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.url.replace('/assets/accounts/', '/assets/account-secrets/')
|
||||
}
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
account: {},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
columns: [
|
||||
'hostname', 'ip', 'username', 'version', 'connectivity',
|
||||
'systemuser', 'date_created', 'date_updated', 'actions'
|
||||
],
|
||||
columnsShow: {
|
||||
min: ['username', 'actions'],
|
||||
default: ['hostname', 'ip', 'username', 'version', 'actions']
|
||||
},
|
||||
columnsMeta: {
|
||||
hostname: {
|
||||
prop: 'hostname',
|
||||
label: this.$t('assets.Hostname'),
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
getRoute({ row }) {
|
||||
return {
|
||||
name: 'AssetDetail',
|
||||
params: { id: row.asset }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ip: {
|
||||
width: '120px'
|
||||
},
|
||||
username: {
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
systemuser: {
|
||||
formatter: DisplayFormatter
|
||||
},
|
||||
version: {
|
||||
width: '70px'
|
||||
},
|
||||
connectivity: connectivityMeta,
|
||||
actions: {
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
type: 'primary',
|
||||
callback: function({ row }) {
|
||||
this.account = row
|
||||
this.showViewSecretDialog = true
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.delete(`/api/v1/assets/accounts/${row.id}/`).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: this.$t('common.Test'),
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/accounts/${row.id}/verify/`,
|
||||
{ action: 'test' }
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: !this.$store.getters.currentOrgIsRoot,
|
||||
callback: function({ row }) {
|
||||
this.account = row
|
||||
this.showUpdateSecretDialog = true
|
||||
}.bind(this)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: false,
|
||||
hasImport: false,
|
||||
hasExport: true,
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true
|
||||
},
|
||||
searchConfig: {
|
||||
exclude: ['systemuser', 'asset']
|
||||
},
|
||||
hasSearch: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
Object.assign(this.account, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -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" @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: () => []
|
||||
@@ -52,78 +27,31 @@ export default {
|
||||
moreActionsType: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
moreActionsPlacement: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
// 居中对齐
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
80
src/components/AppAccountListTable/ShowSecretInfo.vue
Normal file
80
src/components/AppAccountListTable/ShowSecretInfo.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<MFAVerifyDialog
|
||||
@MFAVerifyDone="getAuthInfo"
|
||||
@MFAVerifyCancel="exit"
|
||||
/>
|
||||
<Dialog
|
||||
:title="dialogTitle"
|
||||
:show-confirm="false"
|
||||
:show-cancel="false"
|
||||
:destroy-on-close="true"
|
||||
:width="'50'"
|
||||
:visible.sync="showAuthInfo"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div>
|
||||
<el-form label-position="right" label-width="80px" :model="authInfo">
|
||||
<el-form-item :label="this.$t('applications.appName')">
|
||||
<el-input v-model="account['app_display']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="authInfo.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
MFAVerifyDialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogTitle: this.$t('common.ViewSecret'),
|
||||
authInfo: {},
|
||||
showAuthInfo: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAuthInfo()
|
||||
},
|
||||
methods: {
|
||||
getAuthInfo() {
|
||||
const url = `/api/v1/applications/account-secrets/${this.account.id}/`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.authInfo = resp
|
||||
this.showAuthInfo = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
31
src/components/AppAccountListTable/const.js
Normal file
31
src/components/AppAccountListTable/const.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ChoicesFormatter } from '@/components/TableFormatters'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Reachable'),
|
||||
formatter: ChoicesFormatter,
|
||||
formatterArgs: {
|
||||
iconChoices: {
|
||||
ok: 'fa-check text-primary',
|
||||
failed: 'fa-times text-danger',
|
||||
unknown: 'fa-circle text-warning'
|
||||
},
|
||||
hasTips: true,
|
||||
getTips: ({ row, cellValue }) => {
|
||||
const mapper = {
|
||||
'ok': i18n.t('assets.Reachable'),
|
||||
'failed': i18n.t('assets.Unreachable'),
|
||||
'unknown': i18n.t('assets.Unknown')
|
||||
}
|
||||
let tips = mapper[cellValue]
|
||||
if (row['date_verified']) {
|
||||
const datetime = toSafeLocalDateStr(row['date_verified'])
|
||||
tips += '<br> ' + datetime
|
||||
}
|
||||
return tips
|
||||
}
|
||||
},
|
||||
width: '90px',
|
||||
align: 'center'
|
||||
}
|
||||
167
src/components/AppAccountListTable/index.vue
Normal file
167
src/components/AppAccountListTable/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
|
||||
import ShowSecretInfo from './ShowSecretInfo'
|
||||
|
||||
export default {
|
||||
name: 'Detail',
|
||||
components: {
|
||||
ListTable,
|
||||
ShowSecretInfo
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
exportUrl: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.url.replace('/applications/accounts/', '/applications/account-secrets/')
|
||||
}
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
account: {},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
columns: [
|
||||
'app_display', 'username', 'category_display',
|
||||
'type_display', 'systemuser', 'actions'
|
||||
],
|
||||
columnsMeta: {
|
||||
app_display: {
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
getRoute({ row }) {
|
||||
switch (row['category']) {
|
||||
case 'remote_app':
|
||||
return {
|
||||
name: 'RemoteAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
case 'db':
|
||||
return {
|
||||
name: 'DatabaseAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
default:
|
||||
return {
|
||||
name: 'KubernetesAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
username: {
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
systemuser: {
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
getTitle({ row }) {
|
||||
return row.systemuser_display
|
||||
},
|
||||
getRoute({ row }) {
|
||||
return {
|
||||
name: 'SystemUserDetail',
|
||||
params: { id: row.systemuser }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
type: 'primary',
|
||||
callback: function({ row }) {
|
||||
this.account = row
|
||||
this.showViewSecretDialog = true
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: !this.$store.getters.currentOrgIsRoot,
|
||||
callback: function({ row }) {
|
||||
this.$message.success(this.$tc('applications.updateAccountMsg'))
|
||||
}.bind(this)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: false,
|
||||
hasImport: false,
|
||||
hasExport: true,
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true
|
||||
},
|
||||
searchConfig: {
|
||||
exclude: ['systemuser', 'asset']
|
||||
},
|
||||
hasSearch: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
Object.assign(this.account, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,12 +3,12 @@
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<AssetSelect ref="assetSelect" />
|
||||
<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: () => []
|
||||
@@ -49,6 +53,12 @@ export default {
|
||||
onAddSuccess: {
|
||||
type: Function,
|
||||
default: (objects, that) => {}
|
||||
},
|
||||
canSelect: {
|
||||
type: Function,
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -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 {
|
||||
@@ -34,6 +34,16 @@ export default {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
canSelect: {
|
||||
type: Function,
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -66,8 +76,9 @@ export default {
|
||||
select2Config: select2Config,
|
||||
dialogSelect2Config: select2Config,
|
||||
tableConfig: {
|
||||
url: '/api/v1/assets/assets/',
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
hasTree: true,
|
||||
canSelect: this.canSelect,
|
||||
columns: [
|
||||
{
|
||||
prop: 'hostname',
|
||||
@@ -81,8 +92,20 @@ export default {
|
||||
},
|
||||
{
|
||||
prop: 'ip',
|
||||
label: this.$t('assets.ip'),
|
||||
label: this.$t('assets.ipDomain'),
|
||||
sortable: 'custom'
|
||||
},
|
||||
{
|
||||
prop: 'platform',
|
||||
label: this.$t('assets.Platform'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'protocols',
|
||||
formatter: function(row) {
|
||||
return <span> {row.protocols.toString()} </span>
|
||||
},
|
||||
label: this.$t('assets.Protocols')
|
||||
}
|
||||
],
|
||||
listeners: {
|
||||
@@ -156,20 +179,20 @@ export default {
|
||||
.el-select{
|
||||
width: 100%;
|
||||
}
|
||||
.page /deep/ .page-heading{
|
||||
.page ::v-deep .page-heading{
|
||||
display: none;
|
||||
}
|
||||
.el-dialog__wrapper /deep/.el-dialog__body{
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body{
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.page /deep/ .treebox{
|
||||
.page ::v-deep .treebox{
|
||||
height: inherit !important;
|
||||
}
|
||||
.asset-select-dialog >>> .transition-box:first-child {
|
||||
background-color: #f3f3f3 ;
|
||||
}
|
||||
|
||||
.el-dialog__wrapper /deep/.el-dialog__body .wrapper-content {
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body .wrapper-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
<template><div>
|
||||
<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">
|
||||
<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="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>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
name: 'Detail',
|
||||
components: {
|
||||
ListTable,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
handleExport: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
handleImport: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
hasImport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasExport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
MFAConfirmed: false,
|
||||
MFAInput: '',
|
||||
MFAInfo: {
|
||||
asset: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: ''
|
||||
},
|
||||
showDialog: false,
|
||||
showMFADialog: false,
|
||||
dialogInfo: {
|
||||
asset: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
columns: [
|
||||
{
|
||||
prop: 'hostname',
|
||||
label: this.$t('assets.Hostname'),
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'ip',
|
||||
label: this.$t('assets.ip'),
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: this.$t('assets.Username'),
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'version',
|
||||
label: this.$t('assets.Version'),
|
||||
width: '50px'
|
||||
},
|
||||
{
|
||||
prop: 'date_created',
|
||||
label: this.$t('assets.date_joined'),
|
||||
formatter: DateFormatter
|
||||
},
|
||||
{
|
||||
prop: 'id',
|
||||
label: this.$t('common.Action'),
|
||||
align: 'center',
|
||||
width: 150,
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
type: 'primary',
|
||||
callback: function(val) {
|
||||
this.MFAInfo.asset = val.cellValue
|
||||
if (this.MFAVerifyAt + this.MFA_TTl * 1000 > (new Date()).valueOf()) {
|
||||
this.showMFADialog = true
|
||||
this.MFAConfirmed = true
|
||||
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
|
||||
})
|
||||
} else {
|
||||
this.showMFADialog = true
|
||||
}
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
type: 'primary',
|
||||
callback: (val) => {
|
||||
this.$axios.delete(`/api/v1/assets/asset-users/${val.cellValue}/`).then(() => {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
}).catch(() => this.$message.error(this.$t('common.deleteFailedMsg')))
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: this.$t('common.Test'),
|
||||
callback: (val) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/asset-users/tasks/?id=${val.cellValue}`,
|
||||
{ action: 'test' }
|
||||
).then(res => {
|
||||
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
callback: function(val) {
|
||||
this.showDialog = true
|
||||
this.dialogInfo.asset = val.row.asset
|
||||
this.dialogInfo.hostname = val.row.hostname
|
||||
this.dialogInfo.username = val.row.username
|
||||
}.bind(this)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
extraQuery: {
|
||||
latest: 1
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasBulkDelete: false,
|
||||
hasImport: this.hasImport,
|
||||
hasExport: this.hasExport,
|
||||
hasSearch: true,
|
||||
searchConfig: {
|
||||
options: [
|
||||
{
|
||||
label: this.$t('assets.OnlyLatestVersion'),
|
||||
value: 'latest',
|
||||
children: [
|
||||
{
|
||||
label: this.$t('common.Yes'),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: this.$t('common.No'),
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'MFA_TTl',
|
||||
'MFAVerifyAt'
|
||||
]),
|
||||
needMFAVerify() {
|
||||
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
|
||||
return false
|
||||
}
|
||||
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
|
||||
const now = new Date()
|
||||
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.handleExport) {
|
||||
this.headerActions.handleExport = this.handleExport
|
||||
}
|
||||
if (this.handleImport) {
|
||||
this.headerActions.handleImport = this.handleImport
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
MFAConfirm() {
|
||||
if (this.MFAInput.length !== 6) {
|
||||
return this.$message.error(this.$t('common.MFAErrorMsg'))
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/authentication/otp/verify/`, {
|
||||
code: this.MFAInput
|
||||
}
|
||||
).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
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
handleMFAConfirm() {
|
||||
this.MFAInfo = {
|
||||
asset: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: ''
|
||||
}
|
||||
this.MFAInput = ''
|
||||
this.showMFADialog = false
|
||||
this.MFAConfirmed = false
|
||||
},
|
||||
handleCancel() {
|
||||
this.dialogInfo = {
|
||||
asset: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
}
|
||||
this.showDialog = false
|
||||
},
|
||||
Onchange(e) {
|
||||
const vm = this
|
||||
// TODO 校验文件类型
|
||||
const reader = new FileReader()
|
||||
reader.onload = function() {
|
||||
vm.dialogInfo.key = this.result
|
||||
}
|
||||
reader.readAsText(
|
||||
e.target.files[0]
|
||||
)
|
||||
},
|
||||
handleConfirm() {
|
||||
const data = {
|
||||
asset: this.dialogInfo.asset,
|
||||
username: this.dialogInfo.username
|
||||
}
|
||||
if (this.dialogInfo.password !== '') {
|
||||
data.password = this.dialogInfo.password
|
||||
}
|
||||
if (this.dialogInfo.key !== '') {
|
||||
data.key = this.dialogInfo.key
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/asset-users/`,
|
||||
data
|
||||
).then(res => {
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).catch(err => {
|
||||
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
|
||||
})
|
||||
this.dialogInfo = {
|
||||
asset: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
password: '',
|
||||
key: ''
|
||||
}
|
||||
this.showDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</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/DataForm'
|
||||
|
||||
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,
|
||||
hasButtons: 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,20 @@
|
||||
<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"
|
||||
:group="group"
|
||||
:index="i"
|
||||
: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 +36,10 @@ export default {
|
||||
return []
|
||||
}
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
fieldsMeta: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
@@ -38,133 +47,52 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: {},
|
||||
remoteMeta: {},
|
||||
totalFields: [],
|
||||
loading: true,
|
||||
groups: []
|
||||
groups: [],
|
||||
iForm: this.form,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -174,7 +102,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
158
src/components/AutoDataForm/utils.js
Normal file
158
src/components/AutoDataForm/utils.js
Normal file
@@ -0,0 +1,158 @@
|
||||
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],
|
||||
fields: fields
|
||||
})
|
||||
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) {
|
||||
if (this.errors) {
|
||||
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)) {
|
||||
@@ -47,6 +61,7 @@ export default {
|
||||
}
|
||||
const option = {
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
value: name
|
||||
}
|
||||
if (field.type === 'choice' && field.choices) {
|
||||
@@ -67,7 +82,7 @@ export default {
|
||||
{ label: this.$t('common.No'), value: false }
|
||||
]
|
||||
}
|
||||
vm.options.push(option)
|
||||
vm.internalOptions.push(option)
|
||||
}
|
||||
},
|
||||
optionUrlMeta() {
|
||||
@@ -79,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', ['actions', '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>
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
autoParam: ['id=key', 'name=n', 'level=lv'],
|
||||
type: 'get',
|
||||
headers: {
|
||||
'X-JMS-ORG': JSON.parse(this.$cookie.get('jms_current_org')) ? JSON.parse(this.$cookie.get('jms_current_org')).id : ''
|
||||
'X-JMS-ORG': this.$store.getters.currentOrg ? this.$store.getters.currentOrg.id : ''
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
@@ -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]
|
||||
@@ -83,7 +89,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (currentNode) {
|
||||
currentNode.name = currentNode.meta.node.value
|
||||
currentNode.name = currentNode.meta.data.value
|
||||
}
|
||||
this.zTree.editName(currentNode)
|
||||
},
|
||||
@@ -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}`)
|
||||
this.currentNodeId = treeNode.meta.data.id
|
||||
query['node'] = this.currentNodeId
|
||||
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.data.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.data.id
|
||||
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.data.id}&show_current_asset=${show_current_asset}`
|
||||
}
|
||||
this.$router.push({ query })
|
||||
this.$emit('urlChange', url)
|
||||
},
|
||||
removeTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
@@ -115,12 +125,13 @@ export default {
|
||||
return
|
||||
}
|
||||
this.$axios.delete(
|
||||
`${this.treeSetting.nodeUrl}${currentNode.meta.node.id}/`
|
||||
`${this.treeSetting.nodeUrl}${currentNode.meta.data.id}/`
|
||||
).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) {
|
||||
@@ -132,16 +143,14 @@ export default {
|
||||
url,
|
||||
{ 'value': treeNode.name }
|
||||
).then(res => {
|
||||
let assetsAmount = treeNode.meta.node.assetsAmount
|
||||
let assetsAmount = treeNode.meta.data.assetsAmount
|
||||
if (!assetsAmount) {
|
||||
assetsAmount = 0
|
||||
}
|
||||
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)
|
||||
@@ -194,9 +208,9 @@ export default {
|
||||
onDrop: function(event, treeId, treeNodes, targetNode, moveType) {
|
||||
const treeNodesIds = []
|
||||
$.each(treeNodes, function(index, value) {
|
||||
treeNodesIds.push(value.meta.node.id)
|
||||
treeNodesIds.push(value.meta.data.id)
|
||||
})
|
||||
const theUrl = `${this.treeSetting.nodeUrl}${targetNode.meta.node.id}/children/add/`
|
||||
const theUrl = `${this.treeSetting.nodeUrl}${targetNode.meta.data.id}/children/add/`
|
||||
this.$axios.put(
|
||||
theUrl, {
|
||||
nodes: treeNodesIds
|
||||
@@ -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()
|
||||
@@ -215,21 +229,21 @@ export default {
|
||||
}
|
||||
this.zTree.expandNode(parentNode, true, false, true, false)
|
||||
// http://localhost/api/v1/assets/nodes/85aa4ee2-0bd9-41db-9079-aa3646448d0c/children/
|
||||
const url = `${this.treeSetting.nodeUrl}${parentNode.meta.node.id}/children/`
|
||||
const url = `${this.treeSetting.nodeUrl}${parentNode.meta.data.id}/children/`
|
||||
this.$axios.post(url, {}).then(data => {
|
||||
const newNode = {
|
||||
id: data['key'],
|
||||
name: data['value'],
|
||||
pId: parentNode.id,
|
||||
meta: {
|
||||
'node': data
|
||||
data: data
|
||||
}
|
||||
}
|
||||
newNode.checked = this.zTree.getSelectedNodes()[0].checked
|
||||
this.zTree.addNodes(parentNode, 0, newNode)
|
||||
// vm.$refs.dataztree.refresh()
|
||||
const node = this.zTree.getNodeByParam('id', newNode.id, parentNode)
|
||||
this.currentNodeId = node.meta.node.id || newNode.id
|
||||
this.currentNodeId = node.meta.data.id || newNode.id
|
||||
this.zTree.editName(node)
|
||||
this.$message.success(this.$t('common.createSuccessMsg'))
|
||||
}).catch(error => {
|
||||
@@ -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>
|
||||
@@ -97,6 +97,7 @@ export default {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
data: Object,
|
||||
prop: {
|
||||
type: String,
|
||||
@@ -104,10 +105,13 @@ export default {
|
||||
return this.data.id
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types,vue/require-default-prop
|
||||
itemValue: {},
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
value: Object,
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
options: Array
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-form ref="elForm" v-bind="$attrs" :model="value" class="el-form-renderer">
|
||||
<template v-for="item in innerContent">
|
||||
<slot :name="`id:${item.id}`" />
|
||||
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
|
||||
<component
|
||||
:is="item.type === GROUP ? 'render-form-group' : 'render-form-item'"
|
||||
:key="item.id"
|
||||
@@ -13,7 +13,7 @@
|
||||
:options="options[item.id]"
|
||||
@updateValue="updateValue"
|
||||
/>
|
||||
<slot :name="`$id:${item.id}`" />
|
||||
<slot v-if="!isHidden(item)" :name="`$id:${item.id}`" />
|
||||
</template>
|
||||
<slot />
|
||||
</el-form>
|
||||
@@ -202,6 +202,18 @@ export default {
|
||||
setOptions(id, options) {
|
||||
_set(this.options, id, options)
|
||||
this.options = { ...this.options } // 设置之前不存在的 options 时需要重新设置响应式更新
|
||||
},
|
||||
isHidden(item) {
|
||||
if (!item.el || !item.el['hiddenGroup']) {
|
||||
return false
|
||||
}
|
||||
if (item.hidden === true) {
|
||||
return true
|
||||
}
|
||||
if (typeof item.hidden === 'function') {
|
||||
return item.hidden(this.value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:content="fields"
|
||||
:form="basicForm"
|
||||
label-position="right"
|
||||
label-width="17%"
|
||||
label-width="20%"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
@@ -12,9 +12,10 @@
|
||||
<slot v-for="item in fields" :slot="`id:${item.id}`" :name="`id:${item.id}`" />
|
||||
<slot v-for="item in fields" :slot="`$id:${item.id}`" :name="`$id:${item.id}`" />
|
||||
|
||||
<el-form-item>
|
||||
<el-form-item v-if="hasButtons" class="form-buttons">
|
||||
<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>
|
||||
@@ -31,10 +32,18 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasReset: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasSaveContinue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -42,7 +51,7 @@ export default {
|
||||
// 初始值
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => { return {} }
|
||||
default: () => ({})
|
||||
},
|
||||
moreButtons: {
|
||||
type: Array,
|
||||
@@ -60,11 +69,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 +86,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()
|
||||
@@ -88,27 +97,27 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.el-form /deep/ .el-form-item {
|
||||
.el-form ::v-deep .el-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-form /deep/ .el-form-item__content {
|
||||
.el-form ::v-deep .el-form-item__content {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.el-form /deep/ .el-form-item__label {
|
||||
.el-form ::v-deep .el-form-item__label {
|
||||
padding: 0 30px 0 0;
|
||||
}
|
||||
|
||||
.el-form /deep/ .el-form-item__error {
|
||||
.el-form ::v-deep .el-form-item__error {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
.el-form /deep/ .form-group-header {
|
||||
.el-form ::v-deep .form-group-header {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.el-form /deep/ .help-block {
|
||||
.el-form ::v-deep .help-block {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
@@ -116,7 +125,7 @@ export default {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.el-form /deep/ .help-block a {
|
||||
.el-form ::v-deep .help-block a {
|
||||
color: #1c84c6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,9 +11,10 @@
|
||||
v-bind="tableAttrs"
|
||||
:data="data"
|
||||
:row-class-name="rowClassName"
|
||||
v-on="$listeners"
|
||||
@selection-change="selectStrategy.onSelectionChange"
|
||||
@select="selectStrategy.onSelect"
|
||||
@select-all="selectStrategy.onSelectAll($event, selectable)"
|
||||
@select-all="selectStrategy.onSelectAll($event, canSelect)"
|
||||
@sort-change="onSortChange"
|
||||
>
|
||||
<!--TODO 不用jsx写, 感觉template逻辑有点不清晰了-->
|
||||
@@ -90,11 +91,14 @@
|
||||
|
||||
<!--非树-->
|
||||
<template v-else>
|
||||
<el-data-table-column v-if="hasSelection" type="selection" :align="selectionAlign" />
|
||||
<el-data-table-column v-if="hasSelection" type="selection" :align="selectionAlign" :selectable="canSelect" />
|
||||
<el-data-table-column
|
||||
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')
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -713,6 +716,16 @@ export default {
|
||||
hasDetail: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canSelect: {
|
||||
type: Function,
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
totalData: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -800,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: {
|
||||
@@ -818,6 +838,13 @@ export default {
|
||||
* @property {array} rows - 已选中的行数据的数组
|
||||
*/
|
||||
this.$emit('selection-change', val)
|
||||
},
|
||||
totalData(val) {
|
||||
if (val && val.length !== this.total) {
|
||||
this.page = defaultFirstPage
|
||||
this.total = val.length
|
||||
this.getList()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -834,6 +861,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.totalData) {
|
||||
this.getList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery() {
|
||||
@@ -867,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
|
||||
@@ -953,6 +1029,8 @@ export default {
|
||||
})
|
||||
},
|
||||
search(attrs, reset) {
|
||||
// 重置搜索结果到第一页
|
||||
this.page = defaultFirstPage
|
||||
// Orange 重置查询对象
|
||||
if (reset) {
|
||||
this.innerQuery = merge({}, attrs)
|
||||
@@ -962,6 +1040,8 @@ export default {
|
||||
return this.getList()
|
||||
},
|
||||
searchDate(attrs) {
|
||||
// 重置搜索结果到第一页
|
||||
this.page = defaultFirstPage
|
||||
this.innerQuery = merge(this.innerQuery, attrs)
|
||||
return this.getList()
|
||||
},
|
||||
@@ -996,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,16 +1,16 @@
|
||||
.el-data-table /deep/ .el-pagination{
|
||||
.el-data-table ::v-deep .el-pagination{
|
||||
text-align: center !important;
|
||||
}
|
||||
.el-data-table /deep/ .el-table td{
|
||||
.el-data-table ::v-deep .el-table td{
|
||||
padding: 4px 0;
|
||||
}
|
||||
.el-data-table /deep/ .el-table th{
|
||||
.el-data-table ::v-deep .el-table th{
|
||||
padding: 4px 0;
|
||||
}
|
||||
.el-data-table/deep/ .el-form-item{
|
||||
.el-data-table ::v-deep .el-form-item{
|
||||
margin-bottom:10px !important ;
|
||||
margin-top:10px;
|
||||
}
|
||||
.el-data-table/deep/ .el-pagination{
|
||||
.el-data-table ::v-deep .el-pagination{
|
||||
padding:15px 0 !important ;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,25 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
tableConfig() {
|
||||
const config = Object.assign(this.defaultConfig, this.config)
|
||||
const tableDefaultConfig = this.defaultConfig
|
||||
tableDefaultConfig.paginationSize = _.get(this.globalTableConfig, 'paginationSize', 15)
|
||||
let tableAttrs = tableDefaultConfig.tableAttrs
|
||||
if (this.config.tableAttrs) {
|
||||
tableAttrs = Object.assign(tableAttrs, this.config.tableAttrs)
|
||||
}
|
||||
const config = Object.assign(tableDefaultConfig, this.config)
|
||||
config.tableAttrs = tableAttrs
|
||||
return config
|
||||
},
|
||||
iListeners() {
|
||||
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
|
||||
}
|
||||
},
|
||||
dataTable() {
|
||||
return this.$refs.table
|
||||
},
|
||||
...mapGetters({
|
||||
'globalTableConfig': 'tableConfig'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
@@ -131,6 +151,14 @@ export default {
|
||||
this.toggleRowSelection(row, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.$store.commit('table/SET_TABLE_CONFIG',
|
||||
{
|
||||
key: 'paginationSize',
|
||||
value: val
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,16 +166,16 @@ export default {
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.el-table /deep/ .el-table__row > td {
|
||||
.el-table ::v-deep .el-table__row > td {
|
||||
line-height: 1.5;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.el-table /deep/ .el-table__row > td> div > span {
|
||||
.el-table ::v-deep .el-table__row > td> div > span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.el-table /deep/ .el-table__header > thead > tr >th {
|
||||
.el-table ::v-deep .el-table__header > thead > tr >th {
|
||||
padding: 8px 0;
|
||||
background-color: #F5F5F6;
|
||||
font-size: 13px;
|
||||
@@ -158,11 +186,11 @@ export default {
|
||||
}
|
||||
|
||||
//分页
|
||||
.el-pagination /deep/ .el-pagination__total{
|
||||
.el-pagination ::v-deep .el-pagination__total{
|
||||
float: left;
|
||||
}
|
||||
|
||||
.el-pagination /deep/ .el-pagination__sizes{
|
||||
.el-pagination ::v-deep .el-pagination__sizes{
|
||||
float: left;
|
||||
}
|
||||
//修改颜色
|
||||
|
||||
@@ -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,
|
||||
@@ -39,7 +43,9 @@ export default {
|
||||
iZTreeID: `zTree_${this._uid}`,
|
||||
iRMenuID: `rMenu_${this._uid}`,
|
||||
zTree: '',
|
||||
rMenu: ''
|
||||
rMenu: '',
|
||||
init: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -52,11 +58,30 @@ export default {
|
||||
// $('.treebox').css('height', window.innerHeight - 60)
|
||||
},
|
||||
beforeDestroy() {
|
||||
$.fn.zTree.destroy()
|
||||
$.fn.zTree.destroy(this.iZTreeID)
|
||||
},
|
||||
methods: {
|
||||
initTree: function() {
|
||||
this.$axios.get(this.treeSetting.treeUrl).then(res => {
|
||||
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.$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 = []
|
||||
}
|
||||
@@ -65,7 +90,13 @@ export default {
|
||||
name: this.$t('common.tree.Empty')
|
||||
})
|
||||
}
|
||||
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,
|
||||
@@ -79,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) {
|
||||
@@ -95,7 +129,6 @@ export default {
|
||||
}
|
||||
const refreshIconRef = $('#tree-refresh')
|
||||
refreshIconRef.bind('click', function() {
|
||||
ztree.destroy()
|
||||
const result = callback()
|
||||
if (result && result.then) {
|
||||
result.finally(() => {
|
||||
@@ -158,7 +191,7 @@ export default {
|
||||
top: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
.ztree /deep/ .fa-refresh {
|
||||
.ztree ::v-deep .fa-refresh {
|
||||
font: normal normal normal 14px/1 FontAwesome !important;
|
||||
}
|
||||
.dropdown a:hover {
|
||||
|
||||
@@ -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" />
|
||||
@@ -37,8 +37,8 @@ export default {
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isCopy: true,
|
||||
isMove: true
|
||||
isCopy: false,
|
||||
isMove: false
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
defaultCallback: function(action) {
|
||||
console.log(action)
|
||||
// console.log(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export default {
|
||||
name: 'ItemValue',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Function, Array, Object],
|
||||
type: [String, Number, Function, Array, Object, Boolean],
|
||||
default: ''
|
||||
},
|
||||
item: {
|
||||
@@ -15,15 +15,33 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toChoicesDisplay(value) {
|
||||
if (!value) {
|
||||
return this.$t('common.No')
|
||||
}
|
||||
return this.$t('common.Yes')
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
if (typeof this.formatter === 'function') {
|
||||
return this.formatter(this.item, this.value)
|
||||
}
|
||||
return <span>{this.value}</span>
|
||||
if (typeof this.value === 'boolean') {
|
||||
return (
|
||||
<span class='item-value'>{this.toChoicesDisplay(this.value)}</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span class='item-value'>{this.value}</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-value {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<IBox :title="title" fa="fa-info-circle">
|
||||
<div class="content">
|
||||
<el-row v-if="this.$route.params.id" :gutter="10" class="item">
|
||||
<el-col :span="6"><div :style="{ 'text-align': align }" class="item-label"><label>ID: </label></div></el-col>
|
||||
<el-col :span="18"><div class="item-text">{{ this.$route.params.id }}</div></el-col>
|
||||
</el-row>
|
||||
<el-row v-for="item in items" :key="'card-' + item.key" :gutter="10" class="item">
|
||||
<el-col :span="6"><div :style="{ 'text-align': align }" class="item-label"><label>{{ item.key }}: </label></div></el-col>
|
||||
<el-col :span="18"><div class="item-text">
|
||||
<ItemValue :value="item.value" v-bind="item" />
|
||||
</div></el-col>
|
||||
<el-col :span="6">
|
||||
<div :style="{ 'text-align': align }" class="item-label"><label>{{ item.key }}: </label></div>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<div class="item-text">
|
||||
<ItemValue :value="item.value" v-bind="item" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<slot name="footer">
|
||||
<el-button v-if="showCancel" size="small" @click="onCancel">{{ cancelTitle }}</el-button>
|
||||
<el-button v-if="showConfirm" type="primary" size="small" @click="onConfirm">{{ confirmTitle }}</el-button>
|
||||
<el-button v-if="showConfirm" type="primary" size="small" :loading="loadingStatus" @click="onConfirm">{{ confirmTitle }}</el-button>
|
||||
</slot>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -42,6 +42,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loadingStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
confirmTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
@@ -51,7 +55,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -70,4 +73,8 @@ export default {
|
||||
/*padding-top: 10px;*/
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -93,17 +93,17 @@ export default {
|
||||
|
||||
<style lang='less' scoped>
|
||||
.datepicker{
|
||||
width: 240px;
|
||||
width: 233px;
|
||||
}
|
||||
.el-input__inner{
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 3px;
|
||||
height: 36px;
|
||||
}
|
||||
/*.el-date-editor /deep/ .el-input__icon{*/
|
||||
/*.el-date-editor ::v-deep .el-input__icon{*/
|
||||
/* line-height: 28px;*/
|
||||
/*}*/
|
||||
.el-date-editor /deep/ .el-range-separator{
|
||||
.el-date-editor ::v-deep .el-range-separator{
|
||||
line-height: 28px;
|
||||
}
|
||||
</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>
|
||||
@@ -2,11 +2,13 @@
|
||||
<el-select
|
||||
ref="select"
|
||||
v-model="iValue"
|
||||
v-loading="!initialized"
|
||||
v-loadmore="loadMore"
|
||||
:options="iOptions"
|
||||
:remote="remote"
|
||||
:remote-method="filterOptions"
|
||||
:multiple="multiple"
|
||||
:clearable="clearable"
|
||||
filterable
|
||||
popper-append-to-body
|
||||
class="select2"
|
||||
@@ -69,6 +71,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 初始化值,也就是选中的值
|
||||
value: {
|
||||
type: [Array, String, Number, Boolean],
|
||||
@@ -92,7 +98,6 @@ export default {
|
||||
return {
|
||||
loading: false,
|
||||
initialized: false,
|
||||
iValue: this.value ? this.value : [],
|
||||
defaultParams: _.cloneDeep(defaultParams),
|
||||
params: _.cloneDeep(defaultParams),
|
||||
iOptions: this.options || [],
|
||||
@@ -107,6 +112,17 @@ export default {
|
||||
optionsValues() {
|
||||
return this.iOptions.map((v) => v.value)
|
||||
},
|
||||
iValue: {
|
||||
set(val) {
|
||||
if (!val) {
|
||||
return
|
||||
}
|
||||
this.$emit('input', val)
|
||||
},
|
||||
get() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
iAjax() {
|
||||
const defaultPageSize = 10
|
||||
const defaultMakeParams = (params) => {
|
||||
@@ -156,11 +172,14 @@ export default {
|
||||
this.iValue = iNew
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
// this.$log.debug('Select2 url is: ', this.iAjax.url)
|
||||
if (!this.initialized) {
|
||||
this.initialSelect()
|
||||
this.initialized = true
|
||||
await this.initialSelect()
|
||||
setTimeout(() => {
|
||||
this.initialized = true
|
||||
this.iValue = this.value
|
||||
})
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
// 因为elform存在问题,这个来清楚验证
|
||||
@@ -172,6 +191,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadMore(load) {
|
||||
if (!this.iAjax.url) {
|
||||
return
|
||||
}
|
||||
if (!this.params.hasMore) {
|
||||
return
|
||||
}
|
||||
@@ -235,9 +257,13 @@ export default {
|
||||
// this.$log.debug('Select ajax config', this.iAjax)
|
||||
if (this.iAjax.url) {
|
||||
if (this.value && this.value.length !== 0) {
|
||||
this.$log.debug('Start init select2 value')
|
||||
const data = await createSourceIdCache(this.value)
|
||||
this.params.spm = data.spm
|
||||
this.$log.debug('Start init select2 value, ', this.value)
|
||||
let value = this.value
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value]
|
||||
}
|
||||
const data = await createSourceIdCache(value)
|
||||
this.params.spm = data['spm']
|
||||
await this.getInitialOptions()
|
||||
}
|
||||
await this.getOptions()
|
||||
@@ -6,16 +6,19 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
// value: {
|
||||
// type: String,
|
||||
// default: () => ''
|
||||
// },
|
||||
tip: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
toFormat: {
|
||||
type: String,
|
||||
default: () => 'string'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -26,7 +29,11 @@ export default {
|
||||
const vm = this
|
||||
const reader = new FileReader()
|
||||
reader.onload = function() {
|
||||
vm.$emit('input', this.result)
|
||||
let result = this.result
|
||||
if (vm.toFormat === 'object') {
|
||||
result = JSON.parse(result)
|
||||
}
|
||||
vm.$emit('input', result)
|
||||
}
|
||||
reader.readAsText(
|
||||
e.target.files[0]
|
||||
@@ -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'
|
||||
@@ -22,8 +22,12 @@ export default {
|
||||
}
|
||||
},
|
||||
rules(item) {
|
||||
let userIsOrgAdmin = item.el.userIsOrgAdmin
|
||||
// undefined 个人信息更新或用户更改密码页面,使用当前用户;否则使用更新用户表单中传递的值
|
||||
userIsOrgAdmin = userIsOrgAdmin === undefined ? store.getters.currentUserIsAdmin : userIsOrgAdmin
|
||||
|
||||
const passwordRule = store.getters.publicSettings.PASSWORD_RULE
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
const validatePassword = function(rule, value, callback) {
|
||||
if (!value) {
|
||||
return callback()
|
||||
}
|
||||
@@ -46,7 +50,10 @@ export default {
|
||||
return callback(new Error(msg))
|
||||
}
|
||||
}
|
||||
const secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
|
||||
let secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
|
||||
if (userIsOrgAdmin) {
|
||||
secureLength = passwordRule ? passwordRule.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH : 7
|
||||
}
|
||||
if (value.length < secureLength) {
|
||||
return callback(new Error(i18n.t('common.password.MIN_LENGTH_ERROR', [secureLength])))
|
||||
}
|
||||
@@ -59,17 +66,12 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
attrs: {
|
||||
secureLength: 7
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['publicSettings'])
|
||||
},
|
||||
created() {
|
||||
const passwordRule = this.publicSettings.PASSWORD_RULE || {}
|
||||
this.attrs.secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
|
||||
},
|
||||
methods: {
|
||||
handleInput(value) {
|
||||
this.$emit('input', value)
|
||||
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
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<div class="form-group-header">
|
||||
<div v-if="line" class="hr-line-dashed" />
|
||||
<h3>{{ title }}</h3>
|
||||
<h3>{{ group.title }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Title'
|
||||
},
|
||||
line: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
group: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
vm.tableConfig.initialUrl = vm.tableConfig.url
|
||||
}
|
||||
const initialUrl = vm.tableConfig.initialUrl
|
||||
const nodeId = node.meta.node.id
|
||||
const nodeId = node.meta.data.id
|
||||
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
|
||||
vm.tableConfig.url = url
|
||||
}
|
||||
@@ -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,24 +1,45 @@
|
||||
<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 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>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
<div>
|
||||
<MFAVerifyDialog
|
||||
v-if="mfaDialogShow"
|
||||
@MFAVerifyDone="showExportDialog"
|
||||
@MFAVerifyCancel="handleExportCancel"
|
||||
/>
|
||||
<Dialog
|
||||
v-if="exportDialogShow"
|
||||
:title="$t('common.Export')"
|
||||
:visible.sync="exportDialogShow"
|
||||
: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>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
|
||||
|
||||
export default {
|
||||
name: 'ExportDialog',
|
||||
components: {
|
||||
Dialog
|
||||
Dialog,
|
||||
MFAVerifyDialog
|
||||
},
|
||||
props: {
|
||||
selectedRows: {
|
||||
@@ -27,12 +48,20 @@ export default {
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
default: ''
|
||||
},
|
||||
beforeExport: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
mfaVerifyRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
performExport: {
|
||||
type: Function,
|
||||
default(selectedRows, exportOptions, query) {
|
||||
return this.defaultPerformExport(selectedRows, exportOptions, query)
|
||||
default(selectedRows, exportOptions, query, exportType) {
|
||||
return this.defaultPerformExport(selectedRows, exportOptions, query, exportType)
|
||||
}
|
||||
},
|
||||
canExportAll: {
|
||||
@@ -50,9 +79,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showExportDialog: false,
|
||||
exportOption: '',
|
||||
meta: {}
|
||||
exportDialogShow: false,
|
||||
exportOption: 'all',
|
||||
exportTypeOption: 'csv',
|
||||
meta: {},
|
||||
mfaVerified: false,
|
||||
mfaDialogShow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -67,6 +99,8 @@ export default {
|
||||
const query = listTableRef.dataTable.getQuery()
|
||||
delete query['limit']
|
||||
delete query['offset']
|
||||
delete query['date_from']
|
||||
delete query['date_to']
|
||||
return query
|
||||
},
|
||||
tableHasQuery() {
|
||||
@@ -77,7 +111,7 @@ export default {
|
||||
{
|
||||
label: this.$t('common.imExport.ExportAll'),
|
||||
value: 'all',
|
||||
can: this.canExportAll
|
||||
can: this.canExportAll && !this.tableHasQuery
|
||||
},
|
||||
{
|
||||
label: this.$t('common.imExport.ExportOnlySelectedItems'),
|
||||
@@ -90,21 +124,50 @@ 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, name }) => {
|
||||
// Todo: 没有时间了,只能先这么处理了
|
||||
if (url === this.url || url.indexOf(this.url) > -1 || url.indexOf('account') > -1) {
|
||||
this.showExportDialog()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
showExportDialog() {
|
||||
if (!this.mfaVerifyRequired) {
|
||||
this.exportDialogShow = true
|
||||
return
|
||||
}
|
||||
// 这是需要校验 MFA 的
|
||||
if (!this.mfaDialogShow) {
|
||||
this.mfaDialogShow = true
|
||||
} else {
|
||||
this.exportDialogShow = true
|
||||
}
|
||||
},
|
||||
downloadCsv(url) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
async defaultPerformExport(selectRows, exportOption, q) {
|
||||
async defaultPerformExport(selectRows, exportOption, q, exportTypeOption) {
|
||||
const url = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
|
||||
const query = Object.assign({}, q)
|
||||
if (exportOption === 'selected') {
|
||||
@@ -115,13 +178,8 @@ export default {
|
||||
}
|
||||
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'] = 'csv'
|
||||
query['format'] = exportTypeOption
|
||||
const queryStr =
|
||||
(url.indexOf('?') > -1 ? '&' : '?') +
|
||||
queryUtil.stringify(query, '=', '&')
|
||||
@@ -129,17 +187,20 @@ export default {
|
||||
},
|
||||
async handleExport() {
|
||||
const listTableRef = this.$parent.$parent.$parent.$parent
|
||||
const query = listTableRef.dataTable.getQuery()
|
||||
const query = listTableRef['dataTable'].getQuery()
|
||||
delete query['limit']
|
||||
delete query['offset']
|
||||
return this.performExport(this.selectedRows, this.exportOption, query)
|
||||
await this.beforeExport()
|
||||
return this.performExport(this.selectedRows, this.exportOption, query, this.exportTypeOption)
|
||||
},
|
||||
async handleExportConfirm() {
|
||||
await this.handleExport()
|
||||
this.showExportDialog = false
|
||||
this.exportDialogShow = false
|
||||
this.mfaDialogShow = false
|
||||
},
|
||||
handleExportCancel() {
|
||||
this.showExportDialog = false
|
||||
this.exportDialogShow = false
|
||||
this.mfaDialogShow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<ExportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
|
||||
<ImportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
|
||||
<ExportDialog :selected-rows="selectedRows" v-bind="exportOptions" v-on="$listeners" />
|
||||
<ImportDialog :selected-rows="selectedRows" v-bind="importOptions" v-on="$listeners" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@ import ExportDialog from './ExportDialog'
|
||||
import ImportDialog from './ImportDialog'
|
||||
|
||||
export default {
|
||||
name: 'DialogAction',
|
||||
name: 'ImExportDialog',
|
||||
components: {
|
||||
ExportDialog,
|
||||
ImportDialog
|
||||
@@ -20,9 +20,13 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
exportOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
importOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,52 +1,73 @@
|
||||
<template>
|
||||
<Dialog :title="$t('common.Import')" :visible.sync="showImportDialog" :destroy-on-close="true" @confirm="handleImportConfirm" @cancel="handleImportCancel()">
|
||||
<el-form label-position="left" style="padding-left: 50px">
|
||||
<Dialog
|
||||
:title="importTitle"
|
||||
:visible.sync="showImportDialog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:loading-status="loadStatus"
|
||||
width="80%"
|
||||
class="importDialog"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
@close="handleImportCancel"
|
||||
>
|
||||
<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: {
|
||||
@@ -61,82 +82,119 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showImportDialog: false,
|
||||
importOption: '1',
|
||||
isCsv: true,
|
||||
errorMsg: ''
|
||||
importOption: 'create',
|
||||
errorMsg: '',
|
||||
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 baseUrl = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
|
||||
return baseUrl + '?format=csv&template=import&limit=1'
|
||||
},
|
||||
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)
|
||||
})
|
||||
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 = ''
|
||||
@@ -148,33 +206,14 @@ export default {
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
handleImport(item) {
|
||||
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 = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,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>
|
||||
|
||||
427
src/components/ListTable/TableAction/ImportTable.vue
Normal file
427
src/components/ListTable/TableAction/ImportTable.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<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, getUpdateObjURL } 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 = getUpdateObjURL(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%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,34 +1,46 @@
|
||||
<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: {
|
||||
hasCreate: defaultTrue,
|
||||
hasBulkDelete: defaultTrue,
|
||||
hasBulkUpdate: defaultFalse,
|
||||
hasLeftActions: defaultTrue,
|
||||
tableUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasCreate: defaultTrue,
|
||||
canCreate: defaultTrue,
|
||||
createRoute: {
|
||||
type: [String, Object],
|
||||
type: [String, Object, Function],
|
||||
default: function() {
|
||||
return this.$route.name.replace('List', 'Create')
|
||||
}
|
||||
},
|
||||
createInNewPage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hasBulkDelete: defaultTrue,
|
||||
hasBulkUpdate: defaultFalse,
|
||||
hasMoreActions: defaultTrue,
|
||||
tableUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
reloadTable: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
@@ -53,23 +65,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 +122,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iActions() {
|
||||
return [...this.actions, this.moreAction]
|
||||
},
|
||||
actions() {
|
||||
const actions = [...this.defaultActions, ...this.extraActions]
|
||||
return cleanActions(actions, true, {
|
||||
@@ -99,12 +132,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,14 +153,22 @@ 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)
|
||||
this.$log.debug('handle create')
|
||||
if (this.createInNewPage) {
|
||||
const { href } = this.$router.resolve(route)
|
||||
window.open(href, '_blank')
|
||||
} else {
|
||||
this.$router.push(route)
|
||||
}
|
||||
},
|
||||
defaultBulkDeleteCallback({ selectedRows, reloadTable }) {
|
||||
const msg = this.$t('common.deleteWarningMsg') + ' ' + selectedRows.length + ' ' + this.$t('common.rows') + ' ?'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
|
||||
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" />
|
||||
<ImExportDialog
|
||||
:selected-rows="selectedRows"
|
||||
:export-options="iExportOptions"
|
||||
:import-options="iImportOptions"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +14,7 @@
|
||||
import ActionsGroup from '@/components/ActionsGroup'
|
||||
import ImExportDialog from './ImExportDialog'
|
||||
import { cleanActions } from './utils'
|
||||
import { assignIfNot } from '@/utils/common'
|
||||
|
||||
const defaultTrue = { type: Boolean, default: true }
|
||||
|
||||
@@ -24,20 +30,41 @@ export default {
|
||||
default: ''
|
||||
},
|
||||
hasExport: defaultTrue,
|
||||
handleExport: {
|
||||
exportOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
handleExportClick: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showExportDialog', { selectedRows })
|
||||
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl, name: this.name })
|
||||
}
|
||||
},
|
||||
hasImport: defaultTrue,
|
||||
handleImport: {
|
||||
importOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
handleImportClick: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showImportDialog', { selectedRows })
|
||||
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl, name: this.name })
|
||||
}
|
||||
},
|
||||
hasColumnSetting: defaultTrue,
|
||||
handleTableSettingClick: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl, row: selectedRows, name: this.name })
|
||||
}
|
||||
},
|
||||
hasRefresh: defaultTrue,
|
||||
handleRefreshClick: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
this.reloadTable()
|
||||
}
|
||||
},
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -54,12 +81,12 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
defaultRightSideActions: [
|
||||
{ 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 }
|
||||
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleTableSettingClick.bind(this) },
|
||||
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImportClick.bind(this) },
|
||||
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExportClick.bind(this) },
|
||||
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefreshClick.bind(this) }
|
||||
],
|
||||
dialogExportVisible: false,
|
||||
exportValue: 2
|
||||
dialogExportVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -73,20 +100,18 @@ export default {
|
||||
},
|
||||
hasSelectedRows() {
|
||||
return this.selectedRows.length > 0
|
||||
},
|
||||
iImportOptions() {
|
||||
return assignIfNot(this.importOptions, { url: this.tableUrl })
|
||||
},
|
||||
iExportOptions() {
|
||||
const options = assignIfNot(this.exportOptions, { url: this.tableUrl })
|
||||
return options
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTagSearch(val) {
|
||||
this.searchTable(val)
|
||||
},
|
||||
// handleExport({ selectedRows }) {
|
||||
// this.$eventBus.$emit('showExportDialog', { selectedRows })
|
||||
// },
|
||||
// handleImport({ selectedRows }) {
|
||||
// this.$eventBus.$emit('showImportDialog', { selectedRows })
|
||||
// },
|
||||
handleRefresh() {
|
||||
this.reloadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +144,7 @@ export default {
|
||||
}
|
||||
|
||||
.right-side-actions >>> .el-button:hover {
|
||||
background-color: rgb(0, 0, 0, 0.05);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.action-search >>> .el-input__suffix i {
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="headerActions" :selected-rows="selectedRows" :reload-table="reloadTable" />
|
||||
<TableAction
|
||||
:table-url="tableUrl"
|
||||
:search-table="search"
|
||||
:date-pick="handleDateChange"
|
||||
:selected-rows="selectedRows"
|
||||
:reload-table="reloadTable"
|
||||
v-bind="headerActions"
|
||||
/>
|
||||
<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 +26,7 @@ import IBox from '../IBox'
|
||||
import TableAction from './TableAction'
|
||||
import Emitter from '@/mixins/emitter'
|
||||
import deepmerge from 'deepmerge'
|
||||
|
||||
export default {
|
||||
name: 'ListTable',
|
||||
components: {
|
||||
@@ -46,8 +60,12 @@ 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
|
||||
},
|
||||
tableUrl() {
|
||||
return this.iTableConfig.url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -56,10 +74,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 +90,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 +108,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 +120,19 @@ 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-button--text{
|
||||
// color: #409EFF;
|
||||
// }
|
||||
</style>
|
||||
|
||||
77
src/components/MFAVerifyDialog/index.vue
Normal file
77
src/components/MFAVerifyDialog/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:title="$t('common.MFAVerify')"
|
||||
:width="'50'"
|
||||
:show-confirm="false"
|
||||
:show-cancel="false"
|
||||
:visible.sync="visible"
|
||||
:destroy-on-close="true"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-row :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="MFAToken" />
|
||||
<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="verifyMFA">
|
||||
{{ this.$t('common.Confirm') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
|
||||
export default {
|
||||
name: 'MFAVerifyDialog',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
MFAToken: '',
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.$emit('MFAVerifyCancel', true)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$axios.get('/api/v1/authentication/otp/verify/', { disableFlashErrorMsg: true }).then(() => {
|
||||
this.$emit('MFAVerifyDone', true)
|
||||
}).catch(err => {
|
||||
this.$log.debug('Verify otp code error: ', err)
|
||||
this.visible = true
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
verifyMFA() {
|
||||
if (this.MFAToken.length !== 6) {
|
||||
return this.$message.error(this.$tc('common.MFAErrorMsg'))
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/authentication/otp/verify/`, {
|
||||
code: this.MFAToken
|
||||
}
|
||||
).then(res => {
|
||||
this.$emit('MFAVerifyDone', true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Switcher from '../Swicher'
|
||||
import Switcher from '../FormFields/Swicher'
|
||||
export default {
|
||||
name: 'ActionItem',
|
||||
components: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<IBox fa="fa-edit" :title="title" v-bind="$attrs">
|
||||
<div class="quick-actions">
|
||||
<IBox :fa="fa" :title="title" v-bind="$attrs">
|
||||
<div v-for="action of actions" :key="action.title" class="quick-actions">
|
||||
<table>
|
||||
<ActionItem v-for="action of actions" :key="action.title" :action="action" />
|
||||
<ActionItem v-if="action.has === undefined || action.has" :action="action" />
|
||||
</table>
|
||||
</div>
|
||||
</IBox>
|
||||
@@ -20,6 +20,10 @@ export default {
|
||||
ActionItem
|
||||
},
|
||||
props: {
|
||||
fa: {
|
||||
type: String,
|
||||
default: () => 'fa-edit'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default() {
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
<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>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-if="params.hasMore" class="item">
|
||||
<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>
|
||||
@@ -34,10 +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,14 @@ export default {
|
||||
type: [Array, Number, String],
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: null
|
||||
},
|
||||
showHasMore: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
performDelete: {
|
||||
type: Function,
|
||||
default: (obj, that) => {}
|
||||
@@ -130,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
|
||||
},
|
||||
@@ -143,6 +160,12 @@ export default {
|
||||
},
|
||||
hasObjectLeftLength() {
|
||||
return this.totalHasObjectsLength - this.iHasObjects.length
|
||||
},
|
||||
iDisabled() {
|
||||
if (this.disabled !== null) {
|
||||
return this.disabled
|
||||
}
|
||||
return this.currentOrgIsRoot
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
<el-card shadow="never">
|
||||
<div slot="header" class="summary-header">
|
||||
<span class="header-title">{{ title }}</span>
|
||||
<span class="pull-right right-side">
|
||||
<slot name="header-right">
|
||||
<el-tag :type="rightSideLabel.type || 'success'" effect="dark" size="mini">{{ rightSideLabel.title }}</el-tag>
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
<slot>
|
||||
<h1 class="no-margins">
|
||||
<router-link :to="body.route">
|
||||
<span v-if="body.disabled" class="disabled-link">{{ body.count }}</span>
|
||||
<router-link v-else :to="body.route">
|
||||
<span>{{ body.count }}</span>
|
||||
</router-link>
|
||||
</h1>
|
||||
@@ -74,4 +70,8 @@ export default {
|
||||
.no-margins {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.disabled-link {
|
||||
color: #428bca;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<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 }) {
|
||||
const id = row.id
|
||||
const url = `${this.url}${id}/`
|
||||
return this.$axios.delete(url)
|
||||
const url = new URL(this.url, location.origin)
|
||||
url.pathname += `${id}/`
|
||||
const deleteUrl = url.href
|
||||
return this.$axios.delete(deleteUrl)
|
||||
}
|
||||
|
||||
const defaultUpdateCallback = function({ row, col }) {
|
||||
const id = row.id
|
||||
let route = { params: { id: id }}
|
||||
@@ -24,6 +27,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 +84,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 +113,8 @@ export default {
|
||||
type: 'primary',
|
||||
has: colActions.hasUpdate,
|
||||
can: colActions.canUpdate,
|
||||
callback: colActions.onUpdate
|
||||
callback: colActions.onUpdate,
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
@@ -97,7 +122,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 +147,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 +170,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 +193,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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
|
||||
<div slot="content">{{ tipStatus }}<br>{{ tipTime }}</div>
|
||||
<div slot="content" v-html="tips" />
|
||||
<i :class="'fa ' + iconClass" />
|
||||
</el-tooltip>
|
||||
<i v-else :class="'fa ' + iconClass" />
|
||||
@@ -22,18 +22,12 @@ export default {
|
||||
true: 'fa-check text-primary',
|
||||
false: 'fa-times text-danger'
|
||||
},
|
||||
typeChange(val) {
|
||||
return !!val
|
||||
getIconKey({ row, cellValue }) {
|
||||
return cellValue
|
||||
},
|
||||
hasTips: false,
|
||||
tipStatus(val, vm) {
|
||||
if (val.status === 0) {
|
||||
return vm.$t('assets.Unreachable')
|
||||
} else if (val.status === 1) {
|
||||
return vm.$t('assets.Reachable')
|
||||
} else if (val.status === 2) {
|
||||
return vm.$t('assets.Unknown')
|
||||
}
|
||||
getTips: ({ row, cellValue }) => {
|
||||
return cellValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,15 +40,11 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
iconClass() {
|
||||
const key = this.formatterArgs.typeChange(this.cellValue)
|
||||
const key = this.formatterArgs.getIconKey({ row: this.row, cellValue: this.cellValue })
|
||||
return this.formatterArgs.iconChoices[key]
|
||||
},
|
||||
tipStatus() {
|
||||
const vm = this
|
||||
return this.formatterArgs.tipStatus(this.cellValue, vm)
|
||||
},
|
||||
tipTime() {
|
||||
return this.cellValue.datetime
|
||||
tips() {
|
||||
return this.formatterArgs.getTips({ cellValue: this.cellValue, row: this.row })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
export default {
|
||||
name: 'DateFormatter',
|
||||
extends: BaseFormatter,
|
||||
data() {
|
||||
let value
|
||||
if (this.cellValue) {
|
||||
const dt = new Date(this.cellValue)
|
||||
value = this.$d(dt, 'medium')
|
||||
value = toSafeLocalDateStr(this.cellValue)
|
||||
} else {
|
||||
value = ''
|
||||
}
|
||||
@@ -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,18 +11,19 @@ export default {
|
||||
name: 'DeleteActionFormatter',
|
||||
extends: BaseFormatter,
|
||||
computed: {
|
||||
canDelete() {
|
||||
return this.iCanDelete()
|
||||
iDisabled() {
|
||||
// 禁用
|
||||
return (this.disabled() || this.$store.getters.currentOrgIsRoot)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
defaultOnDelete(col, row, cellValue, reload) {
|
||||
const url = col.deleteUrl + cellValue
|
||||
this.$axios.delete(url).then(res => {
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.$message.success(this.$tc('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,10 @@ export default {
|
||||
this.defaultOnDelete(col, row, cellValue, reload)
|
||||
}
|
||||
},
|
||||
iCanDelete() {
|
||||
disabled() {
|
||||
if (this.col.objects === 'all') {
|
||||
return false
|
||||
}
|
||||
return this.col.objects.indexOf(this.cellValue) === -1
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,16 @@ export default {
|
||||
data() {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
return {
|
||||
formatterArgs: formatterArgs,
|
||||
iTitle: formatterArgs.getTitle({ col: this.col, row: this.row, cellValue: this.cellValue })
|
||||
formatterArgs: formatterArgs
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iTitle() {
|
||||
return this.formatterArgs.getTitle({
|
||||
col: this.col,
|
||||
row: this.row,
|
||||
cellValue: this.cellValue
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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
|
||||
}
|
||||
@@ -1,14 +1,35 @@
|
||||
<template>
|
||||
|
||||
<div class="filter-field">
|
||||
<el-cascader ref="Cascade" :options="options" :props="config" @change="handleMenuItemChange" />
|
||||
<el-tag v-for="(v, k) in filterTags" :key="k" :name="k" closable size="small" class="filter-tag" type="info" @close="handleTagClose(k)">
|
||||
<el-tag
|
||||
v-for="(v, k) in filterTags"
|
||||
:key="k"
|
||||
:name="k"
|
||||
closable
|
||||
size="small"
|
||||
class="filter-tag"
|
||||
type="info"
|
||||
:disable-transitions="true"
|
||||
@close="handleTagClose(k)"
|
||||
@click="handleTagClick(v,k)"
|
||||
>
|
||||
<strong v-if="v.label">{{ v.label + ':' }}</strong>
|
||||
<span v-if="v.valueLabel">{{ v.valueLabel }}</span>
|
||||
<span v-else>{{ v.value }}</span>
|
||||
</el-tag>
|
||||
<span v-if="keyLabel" slot="prefix" class="filterTitle">{{ keyLabel + ':' }}</span>
|
||||
<el-input ref="SearchInput" v-model="filterValue" :placeholder="placeholder" class="search-input" @blur="focus = false" @focus="focus = true" @change="handleConfirm" />
|
||||
<el-input
|
||||
ref="SearchInput"
|
||||
v-model="filterValue"
|
||||
:placeholder="placeholder"
|
||||
class="search-input"
|
||||
@blur="focus = false"
|
||||
@focus="focus = true"
|
||||
@change="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -65,13 +86,13 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filterTags: {
|
||||
handler(val) {
|
||||
this.$nextTick(() => this.$emit('tagSearch', this.filterMaps))
|
||||
// this.$emit('tagSearch', this.filterMaps)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
// filterTags: {
|
||||
// handler(val) {
|
||||
// this.$nextTick(() => this.$emit('tagSearch', this.filterMaps))
|
||||
// // this.$emit('tagSearch', this.filterMaps)
|
||||
// },
|
||||
// deep: true
|
||||
// }
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
@@ -116,6 +137,7 @@ export default {
|
||||
},
|
||||
handleTagClose(evt) {
|
||||
this.$delete(this.filterTags, evt)
|
||||
this.$emit('tagSearch', this.filterMaps)
|
||||
return true
|
||||
},
|
||||
handleConfirm() {
|
||||
@@ -124,9 +146,33 @@ export default {
|
||||
}
|
||||
const tag = { key: this.filterKey, label: this.keyLabel, value: this.filterValue, valueLabel: this.valueLabel }
|
||||
this.$set(this.filterTags, this.filterKey, tag)
|
||||
this.$emit('tagSearch', this.filterMaps)
|
||||
this.filterKey = ''
|
||||
this.filterValue = ''
|
||||
this.valueLabel = ''
|
||||
},
|
||||
handleTagClick(v, k) {
|
||||
let unableChange = false
|
||||
for (const field of this.options) {
|
||||
if (field.value === v.key) {
|
||||
if (field.type === 'choice') {
|
||||
unableChange = true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
unableChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unableChange) {
|
||||
return
|
||||
}
|
||||
if (this.filterValue.length !== 0) {
|
||||
this.handleConfirm()
|
||||
}
|
||||
this.$delete(this.filterTags, k)
|
||||
this.filterKey = v.key
|
||||
this.filterValue = v.value
|
||||
this.$refs.SearchInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,25 +65,40 @@ export default {
|
||||
return {
|
||||
iTableConfig: this.tableConfig,
|
||||
iShowTree: this.showTree,
|
||||
componentKey: 0
|
||||
componentKey: 0,
|
||||
componentTreeKey: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
treeConfig: {
|
||||
handler(val) {
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleUrlChange(_url) {
|
||||
this.$set(this.iTableConfig, 'url', _url)
|
||||
this.$emit('urlChange', _url)
|
||||
handleUrlChange(url) {
|
||||
this.$set(this.iTableConfig, 'url', url)
|
||||
this.$emit('urlChange', url)
|
||||
this.forceRerender()
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +115,7 @@ export default {
|
||||
color: #FFFFFF;
|
||||
border-radius: 3px;
|
||||
line-height: 1.428;
|
||||
cursor:pointer;
|
||||
}
|
||||
.el-tree{
|
||||
background-color: inherit !important;
|
||||
|
||||
@@ -12,14 +12,17 @@ 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 AssetUserTable } from './AssetUserTable'
|
||||
export { default as UploadField } from './FormFields/UploadField'
|
||||
export { default as AccountListTable } from './AccountListTable/index'
|
||||
export { default as AppAccountListTable } from './AppAccountListTable'
|
||||
export { default as AssetRelationCard } from './AssetRelationCard'
|
||||
export { default as MFAVerifyDialog } from './MFAVerifyDialog'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,58 @@
|
||||
{
|
||||
"": "",
|
||||
"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": {
|
||||
"": "",
|
||||
"updateAccountMsg": "Please update system user account info",
|
||||
"associateApplication": "Associate application",
|
||||
"RemoteApp": "Remote app",
|
||||
"Database": "Database",
|
||||
"Cloud": "Cloud",
|
||||
"App": "Application",
|
||||
"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",
|
||||
@@ -17,6 +66,7 @@
|
||||
"mysql_workbench": "MySQL Workbench",
|
||||
"mysql_workbench_ip": "DB IP",
|
||||
"mysql_workbench_name": "DB Name",
|
||||
"mysql_workbench_port": "DB Port",
|
||||
"mysql_workbench_username": "DB Account",
|
||||
"mysql_workbench_password": "DB Password",
|
||||
"vmware_client": "vSphere Client",
|
||||
@@ -28,20 +78,37 @@
|
||||
"custom_target": "target URL",
|
||||
"custom_username": "Account",
|
||||
"custom_password": "Password",
|
||||
"Custom": "Custom"
|
||||
"Custom": "Custom",
|
||||
"cluster": "Cluster",
|
||||
"kubernetes":"Kubernetes",
|
||||
"clusterHelpTextMessage": "Tips: https://172.16.8.8:8443",
|
||||
"DBInfo": "Database Info"
|
||||
},
|
||||
"assets": {
|
||||
"AppList": "Application list",
|
||||
"AssociateSystemUsers": "Associate system users",
|
||||
"AssociateAssets": "Associate assets",
|
||||
"AssociateNodes": "Associate nodes",
|
||||
"Action": "Action",
|
||||
"ActiveSelected": "Active selected",
|
||||
"AdminUser": "Admin user",
|
||||
"ReplaceNodeAssetsAdminUser":"Replace node assets admin user with this",
|
||||
"AdminUserDetail": "Admin user detail",
|
||||
"DynamicUsername": "Dynamic username",
|
||||
"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",
|
||||
"LoginOption": "Login option",
|
||||
"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",
|
||||
"AssetNumber": "Asset number",
|
||||
"AssetUserList": "Asset user list",
|
||||
"TestGatewayTestConnection":"Test gateway test connection",
|
||||
"TestGatewayHelpMessage": "If use nat, set the ssh real port",
|
||||
"SshPort": "SSH Port",
|
||||
"Assets": "Assets",
|
||||
"Auth": "Auth",
|
||||
"AutoGenerateKey": "Auto generate ssh key",
|
||||
@@ -57,12 +124,15 @@
|
||||
"CommandFilterRules": "Command filter rules",
|
||||
"Comment": "Comment",
|
||||
"Cpu": "Cpu",
|
||||
"CommonUser": "Common user",
|
||||
"CreatedBy": "Created by",
|
||||
"Database": "Database",
|
||||
"DateJoined": "Date joined",
|
||||
"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",
|
||||
@@ -98,6 +168,7 @@
|
||||
"PublicIp": "Public ip",
|
||||
"Push": "Push",
|
||||
"PushSystemUserNow": "Push system user now",
|
||||
"PushAllSystemUsersToAsset": "Push all system users to asset",
|
||||
"QuickUpdate": "Quick update",
|
||||
"Reachable": "Reachable",
|
||||
"Unreachable": "Unreachable",
|
||||
@@ -106,23 +177,25 @@
|
||||
"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",
|
||||
"Rules": "Rules",
|
||||
"SFTPHelpMessage": "SFTP root dir, tmp, home or custom",
|
||||
"SerialNumber": "Serial number",
|
||||
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig",
|
||||
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami, /bin/ifconfig",
|
||||
"PasswordHelpMessage": "Password or private key password",
|
||||
"SystemUser": "System user",
|
||||
"SystemUserDetail": "System user detail",
|
||||
"SystemUserListHelpMessage": "System user is JumpServer jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); In simple terms, users log into JumpServer using their own username, and JumpServer uses system users to log into assets. When system users are created, if you choose auto push JumpServer to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.\n",
|
||||
"SystemUserListHelpMessage": "<b>System user</b> is the account JumpServer used to log into the asset, such as using root `ssh root@host`, rather than the current user username(ssh admin@host)`;<br><b>Admin user</b> is the account that already exists on an asset, and have privileged permissions, JumpServer using this create common system user, and gather hardware Etc;</br><b>Common user</b> can pre-exist assets or created automatically by the admin user.",
|
||||
"SystemUsers": "System users",
|
||||
"Test": "Test",
|
||||
"TestAssetsConnective": "Test assets connective",
|
||||
"TestAllSystemUsersConnective": "Test all system users 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",
|
||||
@@ -132,13 +205,25 @@
|
||||
"Version": "Version",
|
||||
"command_filter_list": "Command filter list",
|
||||
"date_joined": "Date joined",
|
||||
"sshKeyFingerprint": "SSH fingerprint",
|
||||
"ip": "IP",
|
||||
"sshkey": "sshkey"
|
||||
"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)",
|
||||
"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": {
|
||||
@@ -146,17 +231,38 @@
|
||||
"ReLogin": "Re-Login"
|
||||
},
|
||||
"common": {
|
||||
"Logging": "Logging",
|
||||
"Database": "Database",
|
||||
"Params": "Params",
|
||||
"MFAVerify": "Verify MFA",
|
||||
"ViewSecret": "View secret",
|
||||
"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",
|
||||
"PushSelectedSystemUsersToAsset": "Push selected system users to asset",
|
||||
"TestSelected": "Test selected",
|
||||
"TestSelectedSystemUsersConnective": "Test selected system users connective",
|
||||
"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",
|
||||
@@ -164,8 +270,9 @@
|
||||
"Confirm": "Confirm",
|
||||
"Create": "Create",
|
||||
"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",
|
||||
"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",
|
||||
@@ -179,12 +286,19 @@
|
||||
"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",
|
||||
"MoreActions": "Actions",
|
||||
"Name": "Name",
|
||||
"No": "No",
|
||||
@@ -193,20 +307,26 @@
|
||||
"Other": "Other",
|
||||
"Others": "Others",
|
||||
"Push": "Push",
|
||||
"Receivers": "Receivers",
|
||||
"QuickUpdate": "Quick update",
|
||||
"RemoveSuccessMsg": "Remove success",
|
||||
"Reset": "Reset",
|
||||
"Search": "Search",
|
||||
"MFAErrorMsg": "MFA Error,please check",
|
||||
"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",
|
||||
@@ -218,11 +338,15 @@
|
||||
"bulkDeleteSuccessMsg": "Bulk delete success",
|
||||
"bulkRemoveErrorMsg": "Bulk remove failed: ",
|
||||
"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",
|
||||
"dateExpired": "Date expired",
|
||||
"dateStart": "Date start",
|
||||
"deleteErrorMsg": "Delete failed",
|
||||
@@ -233,6 +357,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",
|
||||
@@ -242,7 +372,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": {
|
||||
@@ -278,7 +411,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",
|
||||
@@ -358,7 +493,7 @@
|
||||
},
|
||||
"perms": {
|
||||
"": "",
|
||||
"Actions": "Actions",
|
||||
"Actions": "Permission",
|
||||
"Asset": "Asset",
|
||||
"Basic": "Basic",
|
||||
"Exclude": "Exclude",
|
||||
@@ -369,59 +504,81 @@
|
||||
"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",
|
||||
"databaseApp": "DatabaseApp",
|
||||
"KubernetesApp": "KubernetesApp",
|
||||
"dateStart": "Date start",
|
||||
"downloadFile": "Download file",
|
||||
"hostName": "Hostname",
|
||||
"isValid": "Validity",
|
||||
"fromTicket": "From ticket",
|
||||
"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",
|
||||
"upDownload": "Upload download",
|
||||
"uploadFile": "Upload file",
|
||||
"clipboardCopyPaste":"Copy Paste",
|
||||
"clipboardCopy":"Clipboard copy",
|
||||
"clipboardPaste":"Clipboard paste",
|
||||
"userCount": "User count",
|
||||
"userGroupCount": "User group count",
|
||||
"usersAndUserGroups": "Users and user groups"
|
||||
},
|
||||
"route": {
|
||||
"": "",
|
||||
"SessionDetail": "SessionDetail",
|
||||
"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",
|
||||
@@ -430,7 +587,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",
|
||||
@@ -438,58 +595,83 @@
|
||||
"DatabaseAppPermissionDetail": "Databases permissions detail",
|
||||
"DatabaseAppPermissionUpdate": "Databases permissions update",
|
||||
"DatabaseAppUpdate": "Database app update",
|
||||
"KubernetesApp": "Kubernetes Apps",
|
||||
"KubernetesAppCreate": "Kubernetes app create",
|
||||
"KubernetesAppDetail": "Kubernetes app detail",
|
||||
"KubernetesAppPermission": "Kubernetes permissions",
|
||||
"KubernetesAppPermissionCreate": "Kubernetes permissions create",
|
||||
"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",
|
||||
"Detail": "Detail",
|
||||
"Activity": "Activity",
|
||||
"SessionOffline": "Sessions offline",
|
||||
"SessionOnline": "Sessions online",
|
||||
"Sessions": "Sessions",
|
||||
"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",
|
||||
"Tickets": "Tickets",
|
||||
"UserCreate": "User create",
|
||||
"UserDetail": "User detail",
|
||||
"UserFirstLogin": "UserFirstLogin",
|
||||
"UserGroupCreate": "User group create",
|
||||
"UserGroupDetail": "User group detail",
|
||||
"UserGroupList": "User groups",
|
||||
"UserGroupList": "User Groups",
|
||||
"UserGroupUpdate": "User group update",
|
||||
"UserGuide": "UserGuide",
|
||||
"UserList": "Users",
|
||||
@@ -497,17 +679,25 @@
|
||||
"UserUpdate": "User update",
|
||||
"Users": "Users",
|
||||
"WebFTP": "WebFTP",
|
||||
"WebTerminal": "Web terminal"
|
||||
"WebTerminal": "Web Terminal",
|
||||
"Notifications": "Notifications",
|
||||
"SiteMessageList": "Site message"
|
||||
},
|
||||
"sessions": {
|
||||
"SetToDefaultStorage": "Set to default storage",
|
||||
"SetToDefault": "Set to default",
|
||||
"SetSuccess": "Set success",
|
||||
"SetFailed": "Set failed",
|
||||
"StorageConfiguration": "Storage configuration",
|
||||
"accountKey": "Account key",
|
||||
"accountName": "Account name",
|
||||
"active": "active",
|
||||
"alive": "alive",
|
||||
"asset": "Asset",
|
||||
"target": "Target",
|
||||
"bucket": "Bucket",
|
||||
"command": "Command",
|
||||
"Activity": "Activity",
|
||||
"commandStorage": "Command storage",
|
||||
"comment": "Comment",
|
||||
"containerName": "Container name",
|
||||
@@ -521,6 +711,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",
|
||||
@@ -544,7 +739,9 @@
|
||||
"systemUser": "System user",
|
||||
"terminalDetail": "Terminal detail",
|
||||
"terminalUpdate": "Update terminal",
|
||||
"terminalUpdateStorage": "Update terminal storage",
|
||||
"terminate": "Terminate",
|
||||
"sessionTerminate": "Session Terminate",
|
||||
"test": "Test",
|
||||
"type": "Type",
|
||||
"user": "Use",
|
||||
@@ -552,9 +749,41 @@
|
||||
"common": "common"
|
||||
},
|
||||
"Monitor": "Monitor",
|
||||
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later"
|
||||
"XRDPNotSupport": "RDP Client session not support now",
|
||||
"sessionMonitor": "Session Monitor",
|
||||
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
|
||||
"helpText": {
|
||||
"esUrl": "Tip: If you have multiple hosts, use comma (, ) to split (eg: http://www.jumpserver.a.com, http://www.jumpserver.b.com)",
|
||||
"esIndex":"Es provides the default index: jumpserver",
|
||||
"esDocType": "Es provides the default document type: command",
|
||||
"s3Endpoint": "S3: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn<br>Example: http://s3.cn-north-1.amazonaws.com.cn",
|
||||
"ossEndpoint": "OSS: http://{REGION_NAME}.aliyuncs.com<br>Example: http://oss-cn-hangzhou.aliyuncs.com"
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"SMSProvider": "SMS provider",
|
||||
"SMS": "SMS setting",
|
||||
"AlibabaCloud": "Alibaba cloud",
|
||||
"TencentCloud": "Tencent cloud",
|
||||
"VerifySignTmpl": "Verification code template",
|
||||
"Radius": "Radius",
|
||||
"Enable": "Enable",
|
||||
"Perm": "Permission",
|
||||
"SMTP": "SMTP server",
|
||||
"Setting": "Setting",
|
||||
"AuthMethod": "Auth methods",
|
||||
"AuthLimit": "Auth limit",
|
||||
"Ops": "Task",
|
||||
"OTP": "OTP(MFA)",
|
||||
"MessageSub": "Message subscription",
|
||||
"Cleaning": "Period clean",
|
||||
"Perms": "授权",
|
||||
"CASSetting": "CAS setting",
|
||||
"Other": "More...",
|
||||
"Auth": "Auth",
|
||||
"SyncSetting": "Sync setting",
|
||||
"Advanced": "Advanced",
|
||||
"InsecureCommandNotifyToSubscription": "Insecure command notification setting, change to system message subscription, support more notify method",
|
||||
"ApiKeyList": "Api key list",
|
||||
"AssetCount": "Asset count",
|
||||
"Basic": "Basic setting",
|
||||
@@ -574,6 +803,8 @@
|
||||
"Security": "Security setting",
|
||||
"SecuritySetting": "Security setting",
|
||||
"SubscriptionID": "Subscription ID",
|
||||
"SystemMessageSubscription": "System messages",
|
||||
"insecureCommandEmailUpdate": "Setting",
|
||||
"Terminal": "Terminal setting",
|
||||
"all": "All",
|
||||
"authLdap": "Enable LDAP auth",
|
||||
@@ -583,11 +814,13 @@
|
||||
"authLdapSearchOu": "User OU",
|
||||
"authLdapServerUri": "LDAP server",
|
||||
"authLdapUserAttrMap": "User attr map",
|
||||
"authCASAttrMap": "User attr map",
|
||||
"unselectedUser": "Unselected user",
|
||||
"auto": "Auto",
|
||||
"basicSetting": "Basic setting",
|
||||
"communityEdition": "Community edition",
|
||||
"consult": "Consult",
|
||||
"createUserSetting": "Create User setting",
|
||||
"CreateUserSetting": "Create User setting",
|
||||
"emailCustomUserCreatedBody": "Create user email content",
|
||||
"emailCustomUserCreatedHonorific": "Create user honorific",
|
||||
"emailCustomUserCreatedSignature": "Signature",
|
||||
@@ -602,11 +835,15 @@
|
||||
"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)",
|
||||
"authLdapSearchOu": "Use | split User OUs",
|
||||
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr",
|
||||
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username, name, email is jumpserver attr",
|
||||
"emailCustomUserCreatedBody": "Tips:When creating a user, send the content of the email",
|
||||
"emailCustomUserCreatedHonorific": "Tips: When creating a user, send the honorific of the email (eg:Hello)",
|
||||
"emailCustomUserCreatedSignature": "Tips: Email signature (eg:jumpserver)",
|
||||
@@ -635,6 +872,11 @@
|
||||
"securityPasswordUpperCase": "After opening, the user password changes and resets must contain uppercase letters",
|
||||
"securityServiceAccountRegistration": "Allow using bootstrap token register service account, when terminal setup, can disable it"
|
||||
},
|
||||
"validatorMessage": {
|
||||
"EnsureThisValueIsGreaterThanOrEqualTo3": "Ensure this value is greater than or equal to 3",
|
||||
"EnsureThisValueIsGreaterThanOrEqualTo5": "Ensure this value is greater than or equal to 5",
|
||||
"EnsureThisValueIsGreaterThanOrEqualTo6": "Ensure this value is greater than or equal to 6"
|
||||
},
|
||||
"import": "Import",
|
||||
"importLdapUserTip": "Please submit the LDAP configuration before import",
|
||||
"importLdapUserTitle": "LDAP user list",
|
||||
@@ -670,19 +912,37 @@
|
||||
"userGuideUrl": "User Guide URL",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Please input username",
|
||||
"refreshLdapCache":"Refreshing Ldap cache "
|
||||
},
|
||||
"settings": {
|
||||
"refreshLdapCache":"Refreshing Ldap cache ",
|
||||
"LicenseExpired": "License expired",
|
||||
"LicenseWillBe": "License will expire at ",
|
||||
"Expire": "",
|
||||
"WeCom": "WeCom",
|
||||
"DingTalk": "DingTalk",
|
||||
"dingTalkTest": "Test",
|
||||
"weComTest": "Test",
|
||||
"FeiShu": "FeiShu",
|
||||
"feiShuTest": "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",
|
||||
@@ -691,7 +951,30 @@
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"Status": "Status",
|
||||
"Open": "Open"
|
||||
"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",
|
||||
"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",
|
||||
@@ -701,12 +984,17 @@
|
||||
"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": {
|
||||
"MessageSubscription": "Message Subscription",
|
||||
"AuthSettings": "Auth settings",
|
||||
"UserName": "Name",
|
||||
"Account": "Account",
|
||||
"Existing":"Existing",
|
||||
"Authentication": "Account",
|
||||
"Comment": "Comment",
|
||||
"ConfirmPassword": "Confirm password",
|
||||
@@ -714,16 +1002,31 @@
|
||||
"DateJoined": "Date joined",
|
||||
"DateLastLogin": "Date last login",
|
||||
"DatePasswordLastUpdated": "Date password last updated",
|
||||
"setWeCom": "Set wecom login",
|
||||
"setDingTalk": "Set dingtalk login",
|
||||
"setFeiShu": "Set feishu login",
|
||||
"DatePasswordUpdated": "Date password updated",
|
||||
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
|
||||
"Email": "Email",
|
||||
"Phone": "Phone",
|
||||
"WeCom": "WeCom",
|
||||
"DingTalk": "DingTalk",
|
||||
"FeiShu": "FeiShu",
|
||||
"FingerPrint": "Fingerprint",
|
||||
"FirstLogin": "First login",
|
||||
"InviteUser": "Invite user",
|
||||
"InviteUserInOrg": "Invite user in this org",
|
||||
"Invite": "Invite",
|
||||
"Guide": "Guide",
|
||||
"OrgUser": "Org User",
|
||||
"OrgAdmin": "Org Admin",
|
||||
"OrgAuditor": "Org Auditor",
|
||||
|
||||
"HelpText": {
|
||||
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "Enable multi-factor authentication to make the account more secure <br/> After is enabled, you will enter the multi-factor authentication binding process on your next login <br/> You can also bind directly in (personal information -> fast modifier -> modifier multiple factor Settings)",
|
||||
"MFAOfUserFirstLoginUserGuidePage": "To protect the security of you and the company <br/> please properly keep your account, password, key and other important and sensitive information <br/> (e.g., set a complex password and enable multi-factor authentication)",
|
||||
"SSHKeyOfProfileSSHUpdatePage": "Copy your public key here"
|
||||
"SSHKeyOfProfileSSHUpdatePage": "Copy your public key here",
|
||||
"OrgRoleHelpText": "Organizational roles are the user's role in the current organization"
|
||||
},
|
||||
"IAgree": "I agree",
|
||||
"ImprovePersonalInformation": "Improve personal information",
|
||||
@@ -742,6 +1045,8 @@
|
||||
"ResetAndDownloadSSHKey": "Reset and download SSH Key",
|
||||
"ResetPublicKeyAndDownload": "Reset public key and download",
|
||||
"Role": "Role",
|
||||
"SuperRole": "Super role",
|
||||
"OrgRole": "Org role",
|
||||
"SSHKey": "SSH Key",
|
||||
"SSHKeySetting": "SSH Key setting",
|
||||
"Secure": "Secure",
|
||||
@@ -758,12 +1063,16 @@
|
||||
"resetMFAWarningMsg": "This will reset the user MFA setting, user can reset it",
|
||||
"resetMFAdSuccessMsg": "Reset MFA success",
|
||||
"resetPassword": "Reset password",
|
||||
"resetPasswordSuccessMsg": "An e-mail has been sent to the user`s mailbox",
|
||||
"resetPasswordSuccessMsg": "A password reset message has been sent to the user",
|
||||
"resetPasswordWarningMsg": "This will reset the user password and send a reset mail",
|
||||
"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"
|
||||
@@ -772,28 +1081,64 @@
|
||||
"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",
|
||||
"SMS": "SMS"
|
||||
},
|
||||
"xpack": {
|
||||
"Admin": "Admin",
|
||||
"Asset": "Asset",
|
||||
"Database": "Database",
|
||||
"AssetCount": "Asset count",
|
||||
"Auditor": "Auditor",
|
||||
"ChangeAuthPlan": {
|
||||
"AddAsset": "Add asset",
|
||||
"AddNode": "Add node",
|
||||
"AddSystemUser": "Add systemuser",
|
||||
"Asset": "Asset",
|
||||
"Database": "Database",
|
||||
"DatabaseId": "Database Id",
|
||||
"SystemUser": "SystemUser",
|
||||
"SystemUserId": "SystemUser Id",
|
||||
"AssetAmount": "Asset",
|
||||
"AssetAndNode": "Asset and Node",
|
||||
"ChangeAuthPlan": "Change auth plan",
|
||||
"ChangeAuthPlanCreate": "Create change auth plan",
|
||||
"ChangeAuthPlanUpdate": "Update change auth plan",
|
||||
"ChangeAuthPlan": "Change Auth Plan",
|
||||
"AssetChangeAuthPlan": "Asset Change Auth Plan",
|
||||
"AppChangeAuthPlan": "App Change Auth Plan",
|
||||
"AssetChangeAuthPlanCreate": "Create Asset change auth plan",
|
||||
"AppChangeAuthPlanCreate": "Create App change auth plan",
|
||||
"AssetChangeAuthPlanUpdate": "Update Asset change auth plan",
|
||||
"AppChangeAuthPlanUpdate": "Update App change auth plan",
|
||||
"SymbolSet": "Special symbol set",
|
||||
"SymbolSetHelpText": "Please enter the special symbol set supported by this type of database. If there are special characters in the generated random password that are not supported by this type of database, the password change plan will fail",
|
||||
"CyclePerform": "Cycle perform",
|
||||
"DateJoined": "Date joined",
|
||||
"DateStart": "Date start",
|
||||
@@ -803,6 +1148,9 @@
|
||||
"ExecutionDetail": "Execution detail",
|
||||
"ExecutionList": "Execution list",
|
||||
"ExecutionTimes": "Execution times",
|
||||
"validatorMessage": {
|
||||
"EnsureThisValueIsGreaterThanOrEqualTo1": "Ensure this value is greater than or equal to 1"
|
||||
},
|
||||
"HelpText": {
|
||||
"CrontabOfCreateUpdatePage": "For example: every Sunday at 03:05 execute <5 3 * * 0> <br/> Using the 5-bit Linux crontab expression <minute hour day month week> (<a href=\"https://tool.lu/crontab/\" target=\"_blank\"> Online tool </a>) <br/> If both regularly perform and cycle perform execution are set, use regularly perform first",
|
||||
"IntervalOfCreateUpdatePage": "Unit: hour",
|
||||
@@ -824,12 +1172,28 @@
|
||||
"Username": "Username"
|
||||
},
|
||||
"Cloud": {
|
||||
"ServerAccountKey": "Server Account Key",
|
||||
"IPNetworkSegment": "Ip Network Segment",
|
||||
"Aliyun": "Ali Cloud",
|
||||
"Qcloud": "Tencent Cloud",
|
||||
"QingyunPrivatecloud": "Qingyun Private Cloud",
|
||||
"HuaweiPrivatecloud": "Huawei Private Cloud",
|
||||
"GCP": "Google Cloud Platform",
|
||||
"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",
|
||||
"AccountList": "Account list",
|
||||
"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",
|
||||
"SyncInstanceTaskCreate": "Create sync instance task",
|
||||
"SyncInstanceTaskList": "Sync instance task list",
|
||||
@@ -868,28 +1232,49 @@
|
||||
"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",
|
||||
"Organization": {
|
||||
"OrganizationCreate": "Create organization",
|
||||
"OrganizationDetail": "Org detail",
|
||||
"OrganizationList": "Organlizations",
|
||||
"OrganizationList": "Organizations",
|
||||
"OrganizationUpdate": "Update org",
|
||||
"OrganizationMembership": "Organization membership",
|
||||
"DeleteOrgTitle":"Please ensure that the following information in the organization has been deleted",
|
||||
"DeleteOrgMsg":"User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission"
|
||||
"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",
|
||||
"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",
|
||||
@@ -906,9 +1291,9 @@
|
||||
"loginImageTip": "Tips: This will be displayed on the enterprise user login page. (suggest image size: 492px*472px)",
|
||||
"loginTitle": "Title of login page",
|
||||
"loginTitleTip": "Tips: This will be displayed on the enterprise user login page. (eg: Welcome to the JumpServer open source fortress)",
|
||||
"logoIndex": "Logo of management page",
|
||||
"logoIndex": "Logo (It contains text)",
|
||||
"logoIndexTip": "Tips: This will appear at the top left of the administration page. (suggest image size: 185px*55px)",
|
||||
"logoLogout": "Logo of logout page",
|
||||
"logoLogout": "Logo (It contains no text)",
|
||||
"logoLogoutTip": "Tips: This will be displayed on the enterprise user logout page. (suggest image size: 82px*82px)",
|
||||
"restoreDialogMessage": "This will restore default Settings of the interface !!!",
|
||||
"restoreDialogTitle": "Are you sure?",
|
||||
|
||||
@@ -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"
|
||||
@@ -13,50 +15,71 @@
|
||||
</template>
|
||||
<script>
|
||||
import AutoDataForm from '@/components/AutoDataForm'
|
||||
import { getUpdateObjURL } from '@/utils/common'
|
||||
export default {
|
||||
name: 'GenericCreateUpdateForm',
|
||||
components: {
|
||||
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 +87,7 @@ export default {
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
// 更新成功的跳转路由
|
||||
updateSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
@@ -71,13 +95,24 @@ 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: {
|
||||
// 获取提交的方法
|
||||
submitMethod: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
const params = this.$route.params
|
||||
@@ -88,25 +123,68 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// 获取创建和更新的url function
|
||||
getUrl: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
const params = this.$route.params
|
||||
let url = this.url
|
||||
if (params.id) {
|
||||
url = `${url}${params.id}/`
|
||||
url = getUpdateObjURL(url, params.id)
|
||||
}
|
||||
return url
|
||||
}
|
||||
},
|
||||
emitPerformSuccessMsg: {
|
||||
type: Function,
|
||||
default(method, res, addContinue) {
|
||||
let msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
|
||||
if (addContinue) {
|
||||
msg = this.saveSuccessContinueMsg
|
||||
}
|
||||
let msgLinkName = ''
|
||||
if (res.name) {
|
||||
msgLinkName = res.name
|
||||
} else if (res.hostname) {
|
||||
msgLinkName = res.hostname
|
||||
}
|
||||
const h = this.$createElement
|
||||
if (this.hasDetailInMsg) {
|
||||
this.$message({
|
||||
message: h('p', null, [
|
||||
h('el-link', {
|
||||
on: {
|
||||
click: () => this.$router.push(this.objectDetailRoute)
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
onPerformSuccess: {
|
||||
type: Function,
|
||||
default(res, method, vm) {
|
||||
const msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
|
||||
default(res, method, vm, addContinue) {
|
||||
const route = this.getNextRoute(res, method)
|
||||
if (!(route.params && route.params.id)) {
|
||||
route['params'] = { 'id': res.id }
|
||||
}
|
||||
this.$emit('submitSuccess', res)
|
||||
this.$message.success(msg)
|
||||
setTimeout(() => this.$router.push(route), 100)
|
||||
|
||||
this.emitPerformSuccessMsg(method, res, addContinue)
|
||||
if (!addContinue) {
|
||||
setTimeout(() => this.$router.push(route), 100)
|
||||
}
|
||||
}
|
||||
},
|
||||
onPerformError: {
|
||||
@@ -125,59 +203,104 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hasSaveContinue: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
hasDetailInMsg: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
loading: true,
|
||||
isSubmitting: false
|
||||
isSubmitting: false,
|
||||
clone: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
method() {
|
||||
return this.getMethod(this)
|
||||
return this.submitMethod(this)
|
||||
},
|
||||
iUrl() {
|
||||
// 更新或创建的url
|
||||
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.isUpdateMethod()
|
||||
}
|
||||
},
|
||||
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) {
|
||||
isUpdateMethod() {
|
||||
return ['put', 'patch'].indexOf(this.method.toLowerCase()) > -1
|
||||
},
|
||||
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.isUpdateMethod() && !cloneFrom) {
|
||||
return Object.assign(this.form, this.initial)
|
||||
}
|
||||
let object = this.object
|
||||
if (object === null) {
|
||||
object = await this.getObjectDetail()
|
||||
if (!object || Object.keys(object).length === 0) {
|
||||
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,7 +1,7 @@
|
||||
<template>
|
||||
<Page>
|
||||
<Page v-bind="$attrs">
|
||||
<IBox>
|
||||
<GenericCreateUpdateForm v-bind="$attrs" v-on="$listeners" />
|
||||
<GenericCreateUpdateForm ref="createUpdateForm" v-bind="$attrs" v-on="$listeners" />
|
||||
</IBox>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -169,8 +175,12 @@ export default {
|
||||
},
|
||||
defaultUpdate() {
|
||||
const id = this.$route.params.id
|
||||
const routeName = this.validActions.updateRoute
|
||||
this.$router.push({ name: routeName, params: { id: id }})
|
||||
let route = this.validActions.updateRoute
|
||||
if (typeof route === 'string') {
|
||||
route = { name: route, params: {}}
|
||||
}
|
||||
route.params.id = id
|
||||
this.$router.push(route)
|
||||
},
|
||||
getObject() {
|
||||
const url = this.validActions.detailApiUrl
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user