mirror of
https://github.com/jumpserver/lina.git
synced 2025-11-08 19:02:40 +00:00
Compare commits
534 Commits
v3.7.2
...
pr@v3@fix_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1dd874562 | ||
|
|
81b3c79ac1 | ||
|
|
710fa9c109 | ||
|
|
b3a3ca13a7 | ||
|
|
e04abd8b79 | ||
|
|
e3a207f7b6 | ||
|
|
40379ac761 | ||
|
|
33862c716e | ||
|
|
4162bdb74d | ||
|
|
d231bd503c | ||
|
|
b42c5f9170 | ||
|
|
f823257515 | ||
|
|
d3f4b2b2e8 | ||
|
|
5eba19946c | ||
|
|
b4e889316e | ||
|
|
4a72e5aa2b | ||
|
|
6ea86c7efe | ||
|
|
dc0a0ae868 | ||
|
|
5b3b8f72cd | ||
|
|
df26679166 | ||
|
|
9efccb8ada | ||
|
|
d784530539 | ||
|
|
afcc60f29c | ||
|
|
f8d581e455 | ||
|
|
dba1540953 | ||
|
|
456227abcf | ||
|
|
66532f4d4b | ||
|
|
42f27eb30f | ||
|
|
57920bf771 | ||
|
|
290772f44e | ||
|
|
f140f2f59e | ||
|
|
7b1883e012 | ||
|
|
352ac7e828 | ||
|
|
1cbd58664c | ||
|
|
e48da6be9b | ||
|
|
fa31b36550 | ||
|
|
6b93a6563d | ||
|
|
d561701049 | ||
|
|
edbf477c1e | ||
|
|
6a2578b339 | ||
|
|
2cb7569cb0 | ||
|
|
9e0c623b9a | ||
|
|
40bf040501 | ||
|
|
efee7c7bbf | ||
|
|
5daecb84ae | ||
|
|
3be325214d | ||
|
|
581509f42f | ||
|
|
654b36b064 | ||
|
|
dcec73ae67 | ||
|
|
00bafa8164 | ||
|
|
da09af79a7 | ||
|
|
b596815ea5 | ||
|
|
cb37273e80 | ||
|
|
c5bf7d0ad2 | ||
|
|
c31195a67a | ||
|
|
1eb59b379a | ||
|
|
b7cee17156 | ||
|
|
f1c8874010 | ||
|
|
5ccaa3b77d | ||
|
|
27d3637330 | ||
|
|
9c8ceb04f0 | ||
|
|
ccd7b319c8 | ||
|
|
55637c7fa1 | ||
|
|
4e95c88318 | ||
|
|
1ff49ca16d | ||
|
|
91c44d0500 | ||
|
|
0b3a9844f7 | ||
|
|
95b58f3c96 | ||
|
|
128b9c79ba | ||
|
|
4eda83f83d | ||
|
|
4cd0071054 | ||
|
|
67a2a9be6a | ||
|
|
f927a2a3cc | ||
|
|
ca40cb34da | ||
|
|
d725e5497d | ||
|
|
56f6c17275 | ||
|
|
e1bde89b29 | ||
|
|
e9da168c9f | ||
|
|
c19ef24ec9 | ||
|
|
fb7c4a8b2a | ||
|
|
428ba49f9c | ||
|
|
7602d6e270 | ||
|
|
7b62ce2d33 | ||
|
|
6ed40c45b0 | ||
|
|
31238e0398 | ||
|
|
676ac2bbf6 | ||
|
|
d5415b84c9 | ||
|
|
5e91917ba4 | ||
|
|
c4361b4c17 | ||
|
|
70b5ec3683 | ||
|
|
c93a061852 | ||
|
|
4351d20a1e | ||
|
|
ce23d53e3c | ||
|
|
32fc16126f | ||
|
|
b9a99148e3 | ||
|
|
2a4b99484b | ||
|
|
bd26894135 | ||
|
|
01e55d7f6e | ||
|
|
d0a7201683 | ||
|
|
e2dcc98ab3 | ||
|
|
8dd4f89395 | ||
|
|
33a997f3bb | ||
|
|
1adee78456 | ||
|
|
baf2b6cd9b | ||
|
|
28e756e163 | ||
|
|
251873a7e9 | ||
|
|
ff9bd42322 | ||
|
|
2c245020cd | ||
|
|
429d2fec40 | ||
|
|
c2f8fe45a1 | ||
|
|
caa5e7df75 | ||
|
|
8a439dec8d | ||
|
|
eabf5a117d | ||
|
|
c30672a014 | ||
|
|
5c5df32181 | ||
|
|
ce831df717 | ||
|
|
04688930ad | ||
|
|
ae5787ae52 | ||
|
|
1dfdfe3932 | ||
|
|
f409abbf79 | ||
|
|
a6232da3d0 | ||
|
|
2690538db6 | ||
|
|
6d0af7a149 | ||
|
|
babc048eb0 | ||
|
|
d49be903e8 | ||
|
|
332058b0ea | ||
|
|
e355abc1af | ||
|
|
99200d58bb | ||
|
|
61bb97efa9 | ||
|
|
c5e030e2fe | ||
|
|
8a60ad774f | ||
|
|
b08e6de527 | ||
|
|
a96851686f | ||
|
|
1b0adbfe71 | ||
|
|
c28419438b | ||
|
|
8807f24bcd | ||
|
|
b8a914eb02 | ||
|
|
8bbc66a281 | ||
|
|
19bab778ca | ||
|
|
134bcda895 | ||
|
|
8b725fa4f6 | ||
|
|
205f8bc280 | ||
|
|
4963446b74 | ||
|
|
98be3903db | ||
|
|
2cb7a859a8 | ||
|
|
d51571c530 | ||
|
|
bf0d19ac0b | ||
|
|
28062f5d60 | ||
|
|
720dee578a | ||
|
|
cc0d78a4e7 | ||
|
|
9f0b904043 | ||
|
|
90cbb25d47 | ||
|
|
450d9562c3 | ||
|
|
a69eec5ef6 | ||
|
|
5f6fc7e3b4 | ||
|
|
62b6ca026e | ||
|
|
006f258938 | ||
|
|
756b7db6b6 | ||
|
|
a8d7c01f94 | ||
|
|
0c7e7ecc99 | ||
|
|
342a70c441 | ||
|
|
815247f5b5 | ||
|
|
e6721a9905 | ||
|
|
d6305fddfd | ||
|
|
6db2d5ae31 | ||
|
|
9b43bba6f8 | ||
|
|
f9d89b30e2 | ||
|
|
be29794b4c | ||
|
|
97bf4f1b97 | ||
|
|
76e7684e26 | ||
|
|
2bfd764da7 | ||
|
|
daf3defe14 | ||
|
|
d8c165ca78 | ||
|
|
42115b2c30 | ||
|
|
447013abf5 | ||
|
|
44216326f9 | ||
|
|
fd64e71a3b | ||
|
|
7b990a264f | ||
|
|
7687917aae | ||
|
|
02b71619de | ||
|
|
da90b23f99 | ||
|
|
b8090abcdc | ||
|
|
45b07dd628 | ||
|
|
ad7a6a5579 | ||
|
|
67c8521d5c | ||
|
|
18fc63b8c0 | ||
|
|
64d2854e49 | ||
|
|
99f83a6bc4 | ||
|
|
ca9f90624e | ||
|
|
05edf98514 | ||
|
|
45b8c622bc | ||
|
|
faf1fb60b7 | ||
|
|
f28825ba74 | ||
|
|
3626fd024d | ||
|
|
79ec0610a8 | ||
|
|
eee5889d51 | ||
|
|
038ec49d5e | ||
|
|
669ffc73fd | ||
|
|
278562e855 | ||
|
|
fafa088a5e | ||
|
|
1ff65a2293 | ||
|
|
1c69c61432 | ||
|
|
046870e366 | ||
|
|
b44af12f3b | ||
|
|
d9a9c7e229 | ||
|
|
55dfa5889b | ||
|
|
b35b5bd774 | ||
|
|
1ee9e5df78 | ||
|
|
2fe6cb37e6 | ||
|
|
da570e21ee | ||
|
|
16adcd299a | ||
|
|
df8a464c36 | ||
|
|
52e121cfdb | ||
|
|
f014fc6426 | ||
|
|
88a6c2bb2b | ||
|
|
8fa31fe0c2 | ||
|
|
8f246c18e1 | ||
|
|
9d999a7119 | ||
|
|
dc8f237fec | ||
|
|
41d0615ab5 | ||
|
|
fadc3e7dd0 | ||
|
|
152f56b496 | ||
|
|
ed4f8dea90 | ||
|
|
dced020a20 | ||
|
|
bf3c87575c | ||
|
|
dadb54090c | ||
|
|
3f683b012c | ||
|
|
ecb1e91136 | ||
|
|
454947f08b | ||
|
|
3ff6c6fe2f | ||
|
|
527cc4d727 | ||
|
|
3b4201d2bf | ||
|
|
ba109da324 | ||
|
|
2a92c7657c | ||
|
|
beb8ace5bd | ||
|
|
5e1225524c | ||
|
|
931042eb2f | ||
|
|
383577bb18 | ||
|
|
f9e94386de | ||
|
|
5879eed926 | ||
|
|
e1e54bf7a3 | ||
|
|
9e0c43589d | ||
|
|
ca3b0cfce5 | ||
|
|
245b3f4ad2 | ||
|
|
ea575e0515 | ||
|
|
ff63d2ca39 | ||
|
|
045af27999 | ||
|
|
257365932c | ||
|
|
aca4e4077f | ||
|
|
ce80d36b8b | ||
|
|
7dd5256303 | ||
|
|
fcf1093b4c | ||
|
|
5f9e9afffb | ||
|
|
1bba9980c2 | ||
|
|
6cbcee7656 | ||
|
|
2084c50f95 | ||
|
|
20d98bf09e | ||
|
|
05c2f1f859 | ||
|
|
1e9107ec4a | ||
|
|
96a3f0a334 | ||
|
|
6938299940 | ||
|
|
0d1eb82fca | ||
|
|
ddf268e8ec | ||
|
|
ae6fb878da | ||
|
|
d7099c118b | ||
|
|
1b73591366 | ||
|
|
6f3f66df73 | ||
|
|
598020a89b | ||
|
|
88486e2b00 | ||
|
|
faf8521c91 | ||
|
|
ccd74fb76f | ||
|
|
d76c6fdbd8 | ||
|
|
af6a55d3f4 | ||
|
|
c052961efe | ||
|
|
57d339f513 | ||
|
|
097771175d | ||
|
|
0922557abc | ||
|
|
6842da1960 | ||
|
|
eb839b4113 | ||
|
|
7e3e8fbf2f | ||
|
|
3800151763 | ||
|
|
0121505f28 | ||
|
|
b9a6f5d3ac | ||
|
|
179b568b16 | ||
|
|
c563697efd | ||
|
|
fa9281aa92 | ||
|
|
cc8d94f666 | ||
|
|
1416405644 | ||
|
|
d29b3effbc | ||
|
|
bd9456ba2d | ||
|
|
0c9d5d9b6b | ||
|
|
b19ddd6799 | ||
|
|
ea48b6ebf3 | ||
|
|
fba2f77874 | ||
|
|
3c900ce387 | ||
|
|
7df11b907f | ||
|
|
cdd51a9c16 | ||
|
|
51a4c013d3 | ||
|
|
111aafb4bb | ||
|
|
81f0b13730 | ||
|
|
a36a9e7645 | ||
|
|
cfe6db6ec5 | ||
|
|
ccb221559a | ||
|
|
ace8501648 | ||
|
|
ff627bb0db | ||
|
|
dba8d84f2d | ||
|
|
e24bfff6ab | ||
|
|
cdcc4226db | ||
|
|
09b52738ca | ||
|
|
e324b16e1b | ||
|
|
1b52e4cb93 | ||
|
|
6ac31b05e5 | ||
|
|
6c24c9a2d9 | ||
|
|
b79ef4f7a8 | ||
|
|
d584f83f59 | ||
|
|
22ed0ec0ca | ||
|
|
436e9e59f1 | ||
|
|
ba63d52275 | ||
|
|
aa48e24881 | ||
|
|
f59b33a27f | ||
|
|
9f87708b96 | ||
|
|
78819e9a04 | ||
|
|
8e981d52eb | ||
|
|
9f4c798ddb | ||
|
|
7eac62635b | ||
|
|
3739d710f8 | ||
|
|
aae5aa9d7f | ||
|
|
88be5d5fe8 | ||
|
|
6ac54c9865 | ||
|
|
5cefbbfb51 | ||
|
|
f10e31bd60 | ||
|
|
e8e8b5bfca | ||
|
|
19adbca7b3 | ||
|
|
ba28d5619a | ||
|
|
68363a4c52 | ||
|
|
f947fa8d36 | ||
|
|
c1d0994781 | ||
|
|
fde576fe8b | ||
|
|
27864be41d | ||
|
|
9d461be4a0 | ||
|
|
bcbfe114d8 | ||
|
|
af8448adeb | ||
|
|
9b7a360ea1 | ||
|
|
3bf7eed52e | ||
|
|
c1d80469db | ||
|
|
e6a1039387 | ||
|
|
ad7b2c4e8f | ||
|
|
d51a787598 | ||
|
|
2ac9183047 | ||
|
|
3b9f0b56fb | ||
|
|
d900a8d5a3 | ||
|
|
6daa16b3ca | ||
|
|
28891234db | ||
|
|
1d7bdcb512 | ||
|
|
0964031922 | ||
|
|
6568f47760 | ||
|
|
448e89c733 | ||
|
|
68dccaa93c | ||
|
|
b0cefb05e0 | ||
|
|
d6bf4c86df | ||
|
|
993ded5ca8 | ||
|
|
951df9e63a | ||
|
|
4b98806b3e | ||
|
|
536520c78c | ||
|
|
63650912b8 | ||
|
|
b65069adeb | ||
|
|
7bd3179488 | ||
|
|
bf6ecca1fa | ||
|
|
69449740df | ||
|
|
591f3ae39a | ||
|
|
463d2b16bf | ||
|
|
cc47d93988 | ||
|
|
19e42c48e4 | ||
|
|
6b37c71b6d | ||
|
|
3af6b1d1fe | ||
|
|
2445ecb6e5 | ||
|
|
a5ae4110da | ||
|
|
dc4378b637 | ||
|
|
91f2de7797 | ||
|
|
29b2829199 | ||
|
|
4e23a34af9 | ||
|
|
2bd1aaa03e | ||
|
|
96e23ddb52 | ||
|
|
351e250688 | ||
|
|
e8ebd1aa64 | ||
|
|
6adca44180 | ||
|
|
f3699069c5 | ||
|
|
2f298a32c3 | ||
|
|
3ba81a43cb | ||
|
|
813f17b52f | ||
|
|
0fc292b1ad | ||
|
|
f3f5bbe114 | ||
|
|
8c732f00e1 | ||
|
|
f91b8c75df | ||
|
|
e86b88bf05 | ||
|
|
bf3a2d748d | ||
|
|
8a4b81e2e5 | ||
|
|
34406ec32d | ||
|
|
3269a2a3ff | ||
|
|
5d50844753 | ||
|
|
97cc73a4fa | ||
|
|
574dae6236 | ||
|
|
84c413f51a | ||
|
|
87a1cce4ca | ||
|
|
be89b5d6f2 | ||
|
|
200c727426 | ||
|
|
d563f66ba1 | ||
|
|
4592eeddd3 | ||
|
|
d73a07dff2 | ||
|
|
e8857bf150 | ||
|
|
0abe33053f | ||
|
|
dc9161c6a7 | ||
|
|
d588be696b | ||
|
|
4b2c806f28 | ||
|
|
e28b026ead | ||
|
|
c7414e0199 | ||
|
|
5a478ebaba | ||
|
|
08577f0ae5 | ||
|
|
38527c6a41 | ||
|
|
01c5b7c4a8 | ||
|
|
b0b3e0d1c9 | ||
|
|
5bc273d057 | ||
|
|
e128576763 | ||
|
|
58f4dca599 | ||
|
|
a6dd7f23c7 | ||
|
|
a267d46464 | ||
|
|
8c829ee498 | ||
|
|
bcfa95de06 | ||
|
|
56745ae944 | ||
|
|
5a664f8c2c | ||
|
|
1fd29e13f8 | ||
|
|
af863dae75 | ||
|
|
b1254d2b87 | ||
|
|
d350cadd4c | ||
|
|
cfe9ebd747 | ||
|
|
03177d4ce9 | ||
|
|
a55b0da22b | ||
|
|
1b0bdf25ae | ||
|
|
c746cdc639 | ||
|
|
3cf6b6f639 | ||
|
|
a1bf8f9ab7 | ||
|
|
5449d74d53 | ||
|
|
dc401f80b9 | ||
|
|
379cd2386a | ||
|
|
4e7bdb9c69 | ||
|
|
41449fb538 | ||
|
|
a75488217c | ||
|
|
8e32792696 | ||
|
|
b47caf0287 | ||
|
|
715ae856f0 | ||
|
|
19ae27e6c2 | ||
|
|
2132bacff5 | ||
|
|
e57c5a20d0 | ||
|
|
3d35f0aafe | ||
|
|
68ac03db9e | ||
|
|
7b5471a451 | ||
|
|
7f052ac85e | ||
|
|
64e75eb9ff | ||
|
|
8e75e5d5e3 | ||
|
|
1810e6833c | ||
|
|
8c7c012785 | ||
|
|
378d82518a | ||
|
|
728b04c8e3 | ||
|
|
b8611f095a | ||
|
|
f49a1184e2 | ||
|
|
77a4441018 | ||
|
|
18f1f0de79 | ||
|
|
d252c7dd08 | ||
|
|
e5547f8a4c | ||
|
|
63d6578991 | ||
|
|
da00ae84a8 | ||
|
|
74a905ee85 | ||
|
|
a03e985df3 | ||
|
|
2d6005a4e0 | ||
|
|
c4ca28d2d9 | ||
|
|
3934c17f52 | ||
|
|
51717f8583 | ||
|
|
67e99702c1 | ||
|
|
27ce5a3785 | ||
|
|
49122bb213 | ||
|
|
263c4d3f89 | ||
|
|
2ed203dc32 | ||
|
|
742a06ea2d | ||
|
|
1ad4e2c62e | ||
|
|
7e29a3e836 | ||
|
|
3a66e8323c | ||
|
|
4852e3d26a | ||
|
|
80509dc15f | ||
|
|
601bd4740c | ||
|
|
62be5885db | ||
|
|
62e49567cc | ||
|
|
f2eff11d66 | ||
|
|
ae61586f95 | ||
|
|
a08fbc3b77 | ||
|
|
53d130f1cf | ||
|
|
c05992ce50 | ||
|
|
52522b7095 | ||
|
|
af3f6c5900 | ||
|
|
7b15ca4955 | ||
|
|
031016330a | ||
|
|
e274640d2e | ||
|
|
bd3352424c | ||
|
|
f8ec2bce0c | ||
|
|
81f34f0154 | ||
|
|
759e205bfb | ||
|
|
fc5029e88a | ||
|
|
6fa8052878 | ||
|
|
0dc62712d4 | ||
|
|
23b08590cf | ||
|
|
8d8ab483e1 | ||
|
|
fb757686e3 | ||
|
|
6a8161dcaf | ||
|
|
ae2391f07f | ||
|
|
50a9ce35ad | ||
|
|
2d2a4be3a2 | ||
|
|
e055429ff2 | ||
|
|
8727bac560 | ||
|
|
fd018dc5ac | ||
|
|
4c3673aef2 | ||
|
|
4eb61373e0 | ||
|
|
8e8dd38e2e | ||
|
|
f8b7720e2c | ||
|
|
50af6fe017 | ||
|
|
8bc617c4a7 | ||
|
|
f75d69601b | ||
|
|
f54d819ec8 | ||
|
|
c50db4089c | ||
|
|
a6d7cc1215 | ||
|
|
f6a2fcbbea | ||
|
|
071822e665 | ||
|
|
063dc9f8e1 | ||
|
|
66e90f189c | ||
|
|
bdd1a86568 | ||
|
|
01d0c9a0c2 |
@@ -22,4 +22,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_KAEL_HOST = 'http://localhost:8083'
|
||||
VUE_APP_ENV = 'development'
|
||||
|
||||
@@ -10,3 +10,4 @@ jobs:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
|
||||
|
||||
36
.github/workflows/release-drafter.yml
vendored
36
.github/workflows/release-drafter.yml
vendored
@@ -19,9 +19,7 @@ jobs:
|
||||
id: get_version
|
||||
run: |
|
||||
TAG=$(basename ${GITHUB_REF})
|
||||
VERSION=${TAG/v/}
|
||||
echo "::set-output name=TAG::$TAG"
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
@@ -33,17 +31,27 @@ jobs:
|
||||
tag: ${{ steps.get_version.outputs.TAG }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.16'
|
||||
|
||||
build-and-release:
|
||||
needs: create-realese
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build it and upload
|
||||
uses: jumpserver/action-build-upload-assets@node14.16
|
||||
node-version: '16.20'
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build web
|
||||
run: |
|
||||
sed -i "s@version-dev@${{ steps.get_version.outputs.TAG }}@g" src/layout/components/NavHeader/About.vue
|
||||
yarn build
|
||||
- name: Create Upload Assets
|
||||
run: |
|
||||
rm -rf build/*
|
||||
mv lina lina-${{ steps.get_version.outputs.TAG }}
|
||||
tar -czf lina-${{ steps.get_version.outputs.TAG }}.tar.gz lina-${{ steps.get_version.outputs.TAG }}
|
||||
echo $(md5sum lina-${{ steps.get_version.outputs.TAG }}.tar.gz | awk '{print $1}') > build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
|
||||
mv lina-${{ steps.get_version.outputs.TAG }}.tar.gz build/
|
||||
- name: Release Upload Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.17.1-bullseye-slim as stage-build
|
||||
FROM node:16.20-bullseye-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
|
||||
20
package.json
20
package.json
@@ -20,26 +20,29 @@
|
||||
"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",
|
||||
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja",
|
||||
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja"
|
||||
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
|
||||
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@ztree/ztree_v3": "3.5.44",
|
||||
"axios": "0.21.1",
|
||||
"axios": "0.28.0",
|
||||
"axios-retry": "^3.1.9",
|
||||
"cron-parser": "^4.0.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"css-color-function": "^1.3.3",
|
||||
"decimal.js": "^10.4.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"echarts": "^4.7.0",
|
||||
"dompurify": "^3.1.6",
|
||||
"echarts": "4.7.0",
|
||||
"element-ui": "2.13.2",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"install": "^0.13.0",
|
||||
"jquery": "^3.6.1",
|
||||
"js-cookie": "2.2.0",
|
||||
"jsencrypt": "^3.2.1",
|
||||
"krry-transfer": "^1.7.3",
|
||||
"less": "^3.10.3",
|
||||
"less-loader": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -54,6 +57,8 @@
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"lodash.values": "^4.3.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-parseformat": "^4.0.0",
|
||||
"normalize.css": "7.0.0",
|
||||
@@ -67,7 +72,6 @@
|
||||
"vue-i18n": "^8.15.5",
|
||||
"vue-json-editor": "^1.4.3",
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-password-strength-meter": "^1.7.2",
|
||||
"vue-router": "3.0.6",
|
||||
"vue-select": "^3.9.5",
|
||||
@@ -79,7 +83,7 @@
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.0.0",
|
||||
"@babel/core": "7.18.6",
|
||||
"@babel/register": "7.0.0",
|
||||
"@vue/cli-plugin-babel": "3.6.0",
|
||||
"@vue/cli-plugin-eslint": "^3.9.1",
|
||||
@@ -93,6 +97,7 @@
|
||||
"chalk": "2.4.2",
|
||||
"compression-webpack-plugin": "^6.1.1",
|
||||
"connect": "3.6.6",
|
||||
"deasync": "^0.1.29",
|
||||
"element-theme-chalk": "^2.13.1",
|
||||
"eslint": "^5.15.3",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
@@ -109,6 +114,7 @@
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"script-loader": "0.7.2",
|
||||
"serve-static": "^1.13.2",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"vue-i18n-extract": "^1.1.1",
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
if(pathname.indexOf('/ui') === -1) {
|
||||
window.location.href = window.location.origin + '/ui/#' + pathname
|
||||
}
|
||||
if (pathname.startsWith('/ui/#/chat')) {
|
||||
window.location.href = window.location.origin + pathname
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -424,7 +424,7 @@ td .el-button.el-button--mini {
|
||||
}
|
||||
|
||||
.el-dialog .el-dialog__body {
|
||||
max-height: 90vh;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,10 @@ export function getCommandFilterList(data) {
|
||||
|
||||
export function getCategoryTypes() {
|
||||
return request({
|
||||
url: '/api/v1/assets/categories/',
|
||||
url: '/api/v1/assets/categories/?limit=1000',
|
||||
method: 'get'
|
||||
}).then(res => {
|
||||
return res.results
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -52,3 +52,22 @@ export function createJob(form) {
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
export function StopJob(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/job-executions/stop/',
|
||||
method: 'post',
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
export function JobUploadFile(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/jobs/upload/',
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60 * 60 * 1000,
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export function getProfile(token) {
|
||||
return request({
|
||||
url: '/api/v1/users/profile/',
|
||||
method: 'get'
|
||||
// params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
BIN
src/assets/img/chat.png
Normal file
BIN
src/assets/img/chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
1
src/assets/img/chatbot.svg
Normal file
1
src/assets/img/chatbot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="transparent" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot "><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>
|
||||
|
After Width: | Height: | Size: 406 B |
BIN
src/assets/img/robot-assistant.png
Normal file
BIN
src/assets/img/robot-assistant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
191
src/components/Apps/AccountCreateUpdateForm/const.js
Normal file
191
src/components/Apps/AccountCreateUpdateForm/const.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
|
||||
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
|
||||
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
|
||||
|
||||
export const accountFieldsMeta = (vm) => {
|
||||
const defaultPrivilegedAccounts = ['root', 'administrator']
|
||||
return {
|
||||
assets: {
|
||||
rules: [Required],
|
||||
component: AssetSelect,
|
||||
label: vm.$t('assets.Asset'),
|
||||
el: {
|
||||
multiple: false
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset
|
||||
}
|
||||
},
|
||||
template: {
|
||||
component: Select2,
|
||||
rules: [Required],
|
||||
el: {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset || !vm.addTemplate
|
||||
}
|
||||
},
|
||||
on_invalid: {
|
||||
rules: [Required],
|
||||
label: vm.$t('accounts.AccountPolicy'),
|
||||
helpText: vm.$t('accounts.BulkCreateStrategy'),
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset
|
||||
}
|
||||
},
|
||||
name: {
|
||||
label: vm.$t('common.Name'),
|
||||
rules: [RequiredChange],
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
if (!vm.usernameChanged) {
|
||||
if (!vm.account?.name) {
|
||||
updateForm({ username: value })
|
||||
}
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
username: {
|
||||
el: {
|
||||
disabled: !!vm.account?.name
|
||||
},
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
vm.usernameChanged = true
|
||||
},
|
||||
change: ([value], updateForm) => {
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
privileged: {
|
||||
label: vm.$t('assets.Privileged'),
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
su_from: {
|
||||
component: Select2,
|
||||
hidden: (formValue) => {
|
||||
return !vm.asset?.id || !vm.iPlatform.su_enabled
|
||||
},
|
||||
el: {
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
ajax: {
|
||||
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${vm.account?.id || ''}&asset=${vm.asset?.id || ''}`,
|
||||
transformOption: (item) => {
|
||||
return { label: `${item.name}(${item.username})`, value: item.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
su_from_username: {
|
||||
label: vm.$t('assets.UserSwitchFrom'),
|
||||
hidden: (formValue) => {
|
||||
return vm.platform || vm.asset || vm.addTemplate
|
||||
}
|
||||
},
|
||||
password: {
|
||||
label: vm.$t('assets.Password'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => {
|
||||
return formValue.secret_type !== 'password' || vm.addTemplate
|
||||
}
|
||||
},
|
||||
ssh_key: {
|
||||
label: vm.$t('assets.PrivateKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
|
||||
},
|
||||
passphrase: {
|
||||
label: vm.$t('assets.Passphrase'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
|
||||
},
|
||||
token: {
|
||||
label: vm.$t('assets.Token'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'token' || vm.addTemplate
|
||||
},
|
||||
access_key: {
|
||||
id: 'access_key',
|
||||
label: vm.$t('assets.AccessKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'access_key' || vm.addTemplate
|
||||
},
|
||||
api_key: {
|
||||
id: 'api_key',
|
||||
label: vm.$t('assets.ApiKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'api_key' || vm.addTemplate
|
||||
},
|
||||
secret_type: {
|
||||
type: 'radio-group',
|
||||
options: [],
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
push_now: {
|
||||
helpText: vm.$t('accounts.AccountPush.WindowsPushHelpText'),
|
||||
hidden: (formValue) => {
|
||||
const automation = vm.iPlatform.automation || {}
|
||||
return !automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!vm.$hasPerm('accounts.push_account') ||
|
||||
(formValue.secret_type === 'ssh_key' && vm.iPlatform.type.value === 'windows') ||
|
||||
vm.addTemplate
|
||||
}
|
||||
},
|
||||
params: {
|
||||
label: vm.$t('assets.PushParams'),
|
||||
component: AutomationParamsForm,
|
||||
el: {},
|
||||
hidden: (formValue) => {
|
||||
const automation = vm.iPlatform.automation || {}
|
||||
vm.fieldsMeta.params.el.method = vm.iPlatform.automation.push_account_method
|
||||
vm.fieldsMeta.params.el.pushAccountParams = vm.iPlatform.automation.push_account_params
|
||||
return !formValue.push_now ||
|
||||
!automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
(formValue.secret_type === 'ssh_key' &&
|
||||
vm.iPlatform.type.value === 'windows') ||
|
||||
!vm.$hasPerm('accounts.push_account') ||
|
||||
vm.addTemplate
|
||||
}
|
||||
},
|
||||
is_active: {
|
||||
label: vm.$t('common.IsActive')
|
||||
},
|
||||
comment: {
|
||||
label: vm.$t('common.Comment'),
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,8 @@
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
|
||||
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
|
||||
export default {
|
||||
name: 'AccountCreateForm',
|
||||
@@ -48,7 +44,6 @@ export default {
|
||||
return {
|
||||
loading: true,
|
||||
usernameChanged: false,
|
||||
defaultPrivilegedAccounts: ['root', 'administrator'],
|
||||
iPlatform: {
|
||||
automation: {},
|
||||
su_enabled: false,
|
||||
@@ -72,178 +67,7 @@ export default {
|
||||
]],
|
||||
[this.$t('common.Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
|
||||
],
|
||||
fieldsMeta: {
|
||||
assets: {
|
||||
rules: [Required],
|
||||
component: AssetSelect,
|
||||
label: this.$t('assets.Asset'),
|
||||
el: {
|
||||
multiple: false
|
||||
},
|
||||
hidden: () => {
|
||||
return this.platform || this.asset
|
||||
}
|
||||
},
|
||||
template: {
|
||||
component: Select2,
|
||||
rules: [Required],
|
||||
el: {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.platform || this.asset || !this.addTemplate
|
||||
}
|
||||
},
|
||||
on_invalid: {
|
||||
rules: [Required],
|
||||
label: this.$t('accounts.AccountPolicy'),
|
||||
helpText: this.$t('accounts.BulkCreateStrategy'),
|
||||
hidden: () => {
|
||||
return this.platform || this.asset
|
||||
}
|
||||
},
|
||||
name: {
|
||||
rules: [RequiredChange],
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
if (!this.usernameChanged) {
|
||||
if (!this.account?.name) {
|
||||
updateForm({ username: value })
|
||||
}
|
||||
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
username: {
|
||||
el: {
|
||||
disabled: !!this.account?.name
|
||||
},
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
this.usernameChanged = true
|
||||
},
|
||||
change: ([value], updateForm) => {
|
||||
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
privileged: {
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
su_from: {
|
||||
component: Select2,
|
||||
hidden: (formValue) => {
|
||||
return !this.asset?.id || !this.iPlatform.su_enabled
|
||||
},
|
||||
el: {
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
ajax: {
|
||||
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${this.account?.id || ''}&asset=${this.asset?.id || ''}`,
|
||||
transformOption: (item) => {
|
||||
return { label: `${item.name}(${item.username})`, value: item.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
su_from_username: {
|
||||
label: this.$t('assets.UserSwitchFrom'),
|
||||
hidden: (formValue) => {
|
||||
return this.platform || this.asset || this.addTemplate
|
||||
}
|
||||
},
|
||||
password: {
|
||||
label: this.$t('assets.Password'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'password' || this.addTemplate
|
||||
},
|
||||
ssh_key: {
|
||||
label: this.$t('assets.PrivateKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
|
||||
},
|
||||
passphrase: {
|
||||
label: this.$t('assets.Passphrase'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
|
||||
},
|
||||
token: {
|
||||
label: this.$t('assets.Token'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'token' || this.addTemplate
|
||||
},
|
||||
access_key: {
|
||||
id: 'access_key',
|
||||
label: this.$t('assets.AccessKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'access_key' || this.addTemplate
|
||||
},
|
||||
api_key: {
|
||||
id: 'api_key',
|
||||
label: this.$t('assets.ApiKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'api_key' || this.addTemplate
|
||||
},
|
||||
secret_type: {
|
||||
type: 'radio-group',
|
||||
options: [],
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
push_now: {
|
||||
helpText: this.$t('accounts.AccountPush.WindowsPushHelpText'),
|
||||
hidden: (formValue) => {
|
||||
const automation = this.iPlatform.automation || {}
|
||||
return !automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!this.$hasPerm('accounts.push_account') ||
|
||||
(formValue.secret_type === 'ssh_key' && this.iPlatform.type.value === 'windows') ||
|
||||
this.addTemplate
|
||||
}
|
||||
},
|
||||
params: {
|
||||
label: this.$t('assets.PushParams'),
|
||||
component: AutomationParamsForm,
|
||||
el: {
|
||||
method: this.asset?.auto_config?.push_account_method
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
const automation = this.iPlatform.automation || {}
|
||||
return !formValue.push_now ||
|
||||
!automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!this.$hasPerm('accounts.push_account') ||
|
||||
this.addTemplate
|
||||
}
|
||||
},
|
||||
comment: {
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
}
|
||||
},
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
hasSaveContinue: false
|
||||
}
|
||||
},
|
||||
@@ -251,11 +75,18 @@ export default {
|
||||
try {
|
||||
await this.getPlatform()
|
||||
this.setSecretTypeOptions()
|
||||
this.getDefaultAssets()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getDefaultAssets() {
|
||||
const assetId = this.$route.query.asset_id
|
||||
if (assetId && !this.form.name) {
|
||||
this.form.assets = [assetId]
|
||||
}
|
||||
},
|
||||
async getPlatform() {
|
||||
if (this.platform) {
|
||||
this.iPlatform = this.platform
|
||||
@@ -320,6 +151,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<GenericUpdateFormDialog
|
||||
v-if="visible"
|
||||
:form-setting="formSetting"
|
||||
:selected-rows="selectedRows"
|
||||
:visible="visible"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GenericUpdateFormDialog } from '@/layout/components'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'AccountBulkUpdateDialog',
|
||||
components: {
|
||||
GenericUpdateFormDialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formSetting: {
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
hasSaveContinue: false,
|
||||
fields: [],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
cleanOtherFormValue: (formValue) => {
|
||||
for (const value of formValue) {
|
||||
Object.keys(value).forEach((item, index, arr) => {
|
||||
if (['ssh_key', 'token', 'access_key', 'api_key', 'password'].includes(item)) {
|
||||
value['secret'] = encryptPassword(value[item])
|
||||
delete value[item]
|
||||
}
|
||||
})
|
||||
}
|
||||
return formValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.filterFieldsMeta()
|
||||
},
|
||||
methods: {
|
||||
filterFieldsMeta() {
|
||||
let fields = ['privileged']
|
||||
const fieldsMeta = {}
|
||||
const secretFields = ['password', 'ssh_key', 'passphrase', 'token', 'access_key', 'api_key']
|
||||
const secret_type = this.selectedRows[0].secret_type?.value || 'password'
|
||||
for (const field of secretFields) {
|
||||
if (secret_type === 'ssh_key' && field === 'passphrase') {
|
||||
fields.push('passphrase')
|
||||
this.formSetting.fieldsMeta['passphrase'].hidden = () => false
|
||||
continue
|
||||
}
|
||||
if (secret_type === field) {
|
||||
fields.push(field)
|
||||
this.formSetting.fieldsMeta[field].hidden = () => false
|
||||
continue
|
||||
}
|
||||
delete this.formSetting.fieldsMeta[field]
|
||||
}
|
||||
fields = fields.concat(['is_active', 'comment'])
|
||||
for (const field of fields) {
|
||||
fieldsMeta[field] = this.formSetting.fieldsMeta[field]
|
||||
}
|
||||
this.formSetting.fields = fields
|
||||
this.formSetting.fieldsMeta = fieldsMeta
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,7 +8,6 @@
|
||||
:title="title"
|
||||
:visible.sync="iVisible"
|
||||
v-bind="$attrs"
|
||||
width="70%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AccountCreateUpdateForm
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
:result="createAccountResults"
|
||||
:visible.sync="showResultDialog"
|
||||
/>
|
||||
<AccountBulkUpdateDialog
|
||||
v-if="updateSelectedDialogSetting.visible"
|
||||
:visible.sync="updateSelectedDialogSetting.visible"
|
||||
v-bind="updateSelectedDialogSetting"
|
||||
@update="handleAccountBulkUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,10 +55,12 @@ import AccountCreateUpdate from './AccountCreateUpdate.vue'
|
||||
import { connectivityMeta } from './const'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import ResultDialog from './BulkCreateResultDialog.vue'
|
||||
import AccountBulkUpdateDialog from '@/components/Apps/AccountListTable/AccountBulkUpdateDialog.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccountListTable',
|
||||
components: {
|
||||
AccountBulkUpdateDialog,
|
||||
ResultDialog,
|
||||
ListTable,
|
||||
UpdateSecretInfo,
|
||||
@@ -117,6 +125,10 @@ export default {
|
||||
headerExtraActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
extraQuery: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -138,9 +150,7 @@ export default {
|
||||
app: 'assets',
|
||||
resource: 'account'
|
||||
},
|
||||
extraQuery: {
|
||||
order: '-date_updated'
|
||||
},
|
||||
extraQuery: this.extraQuery,
|
||||
columnsExclude: ['spec_info'],
|
||||
columnsShow: {
|
||||
min: ['name', 'username', 'actions'],
|
||||
@@ -239,7 +249,7 @@ export default {
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: this.$t('common.Test'),
|
||||
title: this.$t('accounts.Test'),
|
||||
can: ({ row }) =>
|
||||
!this.$store.getters.currentOrgIsRoot &&
|
||||
this.$hasPerm('accounts.change_account') &&
|
||||
@@ -279,6 +289,7 @@ export default {
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLabelSearch: true,
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: true,
|
||||
hasCreate: false,
|
||||
@@ -338,6 +349,29 @@ export default {
|
||||
...this.headerExtraActions
|
||||
],
|
||||
extraMoreActions: [
|
||||
{
|
||||
name: 'BulkVerify',
|
||||
title: this.$t('accounts.BulkVerify'),
|
||||
type: 'primary',
|
||||
fa: 'fa-link',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 &&
|
||||
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
|
||||
!this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => {
|
||||
return v.id
|
||||
})
|
||||
this.$axios.post(
|
||||
'/api/v1/accounts/accounts/tasks/',
|
||||
{ action: 'verify', accounts: ids }).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
}).catch(err => {
|
||||
this.$message.error(this.$tc('common.bulkVerifyErrorMsg' + ' ' + err))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'ClearSecrets',
|
||||
title: this.$t('common.ClearSecret'),
|
||||
@@ -347,7 +381,9 @@ export default {
|
||||
return selectedRows.length > 0 && vm.$hasPerm('accounts.change_account')
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => { return v.id })
|
||||
const ids = selectedRows.map(v => {
|
||||
return v.id
|
||||
})
|
||||
this.$axios.patch(
|
||||
'/api/v1/accounts/accounts/clear-secret/',
|
||||
{ account_ids: ids }).then(() => {
|
||||
@@ -356,6 +392,21 @@ export default {
|
||||
this.$message.error(this.$tc('common.bulkClearErrorMsg' + ' ' + err))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'actionUpdateSelected',
|
||||
title: this.$t('accounts.AccountBatchUpdate'),
|
||||
fa: 'batch-update',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 &&
|
||||
!this.$store.getters.currentOrgIsRoot &&
|
||||
vm.$hasPerm('accounts.change_account') &&
|
||||
selectedRows.every(i => i.secret_type.value === selectedRows[0].secret_type.value)
|
||||
},
|
||||
callback: ({ selectedRows }) => {
|
||||
vm.updateSelectedDialogSetting.selectedRows = selectedRows
|
||||
vm.updateSelectedDialogSetting.visible = true
|
||||
}
|
||||
}
|
||||
],
|
||||
canBulkDelete: vm.$hasPerm('accounts.delete_account'),
|
||||
@@ -364,6 +415,10 @@ export default {
|
||||
exclude: ['asset']
|
||||
},
|
||||
hasSearch: true
|
||||
},
|
||||
updateSelectedDialogSetting: {
|
||||
visible: false,
|
||||
selectedRows: []
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -391,9 +446,18 @@ export default {
|
||||
can: this.$hasPerm('accounts.delete_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
const msg = this.$t('accounts.AccountDeleteConfirmMsg')
|
||||
this.$confirm(msg, this.$tc('common.Info'), {
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
beforeClose: async(action, instance, done) => {
|
||||
if (action !== 'confirm') return done()
|
||||
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
|
||||
done()
|
||||
this.$refs.ListTable.reloadTable()
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -422,6 +486,10 @@ export default {
|
||||
setTimeout(() => {
|
||||
this.showResultDialog = true
|
||||
}, 100)
|
||||
},
|
||||
handleAccountBulkUpdate() {
|
||||
this.updateSelectedDialogSetting.visible = false
|
||||
this.$refs.ListTable.reloadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
src/components/Apps/AccountListTable/RemoveAccount.vue
Normal file
112
src/components/Apps/AccountListTable/RemoveAccount.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:visible.sync="show"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'RemoveAccount',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
accounts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
mfaDialogVisible: true
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
const url = `/api/v1/accounts/accounts/tasks/`
|
||||
this.$axios.post(
|
||||
url, { disableFlashErrorMsg: true, action: 'remove' }
|
||||
).then(resp => {
|
||||
this.$axios.post(
|
||||
`/api/v1/accounts/accounts/tasks/`,
|
||||
{
|
||||
action: 'remove',
|
||||
gather_accounts: this.accounts.map(account => account.id)
|
||||
}
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
accountConfirmHandle() {
|
||||
this.show = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-textarea > > > .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
> > > .el-form-item__label {
|
||||
padding-right: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
> > > .el-form-item__content {
|
||||
line-height: 30px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="mfaDialogVisible">
|
||||
<UserConfirmDialog
|
||||
:url="url"
|
||||
@UserConfirmCancel="exit"
|
||||
@UserConfirmDone="getAuthInfo"
|
||||
/>
|
||||
</div>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
@@ -67,7 +60,6 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
@@ -76,7 +68,6 @@ export default {
|
||||
components: {
|
||||
Dialog,
|
||||
PasswordHistoryDialog,
|
||||
UserConfirmDialog,
|
||||
ShowKeyCopyFormatter
|
||||
},
|
||||
props: {
|
||||
@@ -92,6 +83,10 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'account'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
@@ -128,7 +123,10 @@ export default {
|
||||
const url = `/api/v1/accounts/account-secrets/${this.account.id}/histories/?limit=1`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.versions = resp.count
|
||||
this.showSecretDialog()
|
||||
})
|
||||
} else {
|
||||
this.showSecretDialog()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -142,14 +140,15 @@ export default {
|
||||
name: this.secretInfo.name,
|
||||
secret: encryptPassword(this.modifiedSecret)
|
||||
}
|
||||
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, params).then(() => {
|
||||
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
})
|
||||
},
|
||||
getAuthInfo() {
|
||||
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.secretInfo = resp
|
||||
this.sshKeyFingerprint = resp?.spec_info?.ssh_key_fingerprint || '-'
|
||||
showSecretDialog() {
|
||||
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
|
||||
this.secretInfo = res
|
||||
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
|
||||
this.showSecret = true
|
||||
})
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<AssetSelect ref="assetSelect" :can-select="canSelect" :disabled="disabled" />
|
||||
<AssetSelect ref="assetSelect" :can-select="canSelect" :disabled="disabled" :tree-setting="treeSetting" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -59,6 +59,10 @@ export default {
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:loading-status="!isLoaded"
|
||||
:title="$tc('assets.Assets')"
|
||||
custom-class="asset-select-dialog"
|
||||
top="1vh"
|
||||
top="2vh"
|
||||
v-bind="$attrs"
|
||||
width="80vw"
|
||||
width="1000px"
|
||||
@cancel="handleCancel"
|
||||
@close="handleClose"
|
||||
@confirm="handleConfirm"
|
||||
@@ -17,8 +19,10 @@
|
||||
:table-config="tableConfig"
|
||||
:tree-url="`${baseNodeUrl}children/tree/`"
|
||||
:url="baseUrl"
|
||||
:tree-setting="treeSetting"
|
||||
class="tree-table"
|
||||
v-bind="$attrs"
|
||||
@loaded="handleTableLoaded"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -52,11 +56,16 @@ export default {
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
isLoaded: false,
|
||||
dialogVisible: false,
|
||||
rowSelected: _.cloneDeep(this.value) || [],
|
||||
rowsAdd: [],
|
||||
@@ -83,16 +92,6 @@ export default {
|
||||
return row.platform.name
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'protocols',
|
||||
formatter: function(row) {
|
||||
const data = row.protocols.map(p => {
|
||||
return <el-tag size='mini'>{p.name}/{p.port} </el-tag>
|
||||
})
|
||||
return <span> {data} </span>
|
||||
},
|
||||
label: this.$t('assets.Protocols')
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
has: false
|
||||
@@ -114,6 +113,7 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasRightActions: false,
|
||||
hasLabelSearch: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: false
|
||||
}
|
||||
@@ -146,6 +146,9 @@ export default {
|
||||
if (selectValueIndex > -1) {
|
||||
this.rowSelected.splice(selectValueIndex, 1)
|
||||
}
|
||||
},
|
||||
handleTableLoaded() {
|
||||
this.isLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +171,7 @@ export default {
|
||||
}
|
||||
|
||||
.right {
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
ref="dialog"
|
||||
:base-node-url="baseNodeUrl"
|
||||
:base-url="baseUrl"
|
||||
:tree-setting="treeSetting"
|
||||
:tree-url-query="treeUrlQuery"
|
||||
:value="value"
|
||||
:visible.sync="dialogVisible"
|
||||
@@ -27,7 +28,6 @@
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelectDialog from './dialog.vue'
|
||||
import { b } from 'css-color-function/lib/adjusters'
|
||||
|
||||
export default {
|
||||
componentName: 'AssetSelect',
|
||||
@@ -48,6 +48,10 @@ export default {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -76,7 +80,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
b,
|
||||
handleFocus() {
|
||||
this.$refs.select2.selectRef.blur()
|
||||
this.dialogVisible = true
|
||||
@@ -134,7 +137,8 @@ export default {
|
||||
padding: 5px;
|
||||
|
||||
.ztree {
|
||||
height: calc(100vh - 250px) !important;
|
||||
min-height: 500px;
|
||||
height: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ export default {
|
||||
type: String,
|
||||
default: '/api/v1/assets/assets/'
|
||||
},
|
||||
typeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/category/tree/'
|
||||
},
|
||||
nodeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/'
|
||||
@@ -60,6 +64,7 @@ export default {
|
||||
const showAssets = this.treeSetting?.showAssets || this.showAssets
|
||||
const treeUrlQuery = this.setTreeUrlQuery()
|
||||
const assetTreeUrl = `${this.treeUrl}?assets=${showAssets ? '1' : '0'}&${treeUrlQuery}`
|
||||
const vm = this
|
||||
|
||||
return {
|
||||
treeTabConfig: {
|
||||
@@ -81,7 +86,13 @@ export default {
|
||||
nodeUrl: this.nodeUrl,
|
||||
treeUrl: assetTreeUrl,
|
||||
callback: {
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode),
|
||||
beforeRefresh: () => {
|
||||
const query = { ...this.$route.query, node_id: '', asset_id: '' }
|
||||
setTimeout(() => {
|
||||
vm.$router.replace({ query: query })
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
...this.treeSetting
|
||||
}
|
||||
@@ -94,9 +105,14 @@ export default {
|
||||
showAssets: false,
|
||||
showSearch: false,
|
||||
customTreeHeaderName: this.$t('assets.BuiltinTree'),
|
||||
url: '/api/v1/assets/nodes/category/tree/',
|
||||
url: this.typeUrl,
|
||||
nodeUrl: this.treeSetting?.nodeUrl || this.nodeUrl,
|
||||
treeUrl: `/api/v1/assets/nodes/category/tree/?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
|
||||
treeUrl: `${this.typeUrl}?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
|
||||
edit: {
|
||||
drag: {
|
||||
isMove: false
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,6 @@ export default {
|
||||
},
|
||||
async mounted() {
|
||||
await this.getUrlMeta()
|
||||
await this.handleFieldChange()
|
||||
},
|
||||
methods: {
|
||||
async getUrlMeta() {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>{{ $tc('common.View') }}</el-button>
|
||||
>
|
||||
{{ $tc('common.View') }}
|
||||
<span>({{ $tc('setting.LockedIP', ipCounts ) }})</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
v-if="visible"
|
||||
:visible.sync="visible"
|
||||
:title="title"
|
||||
width="40%"
|
||||
@@ -54,6 +56,7 @@ export default {
|
||||
remoteMeta: {},
|
||||
visible: false,
|
||||
form: this.value,
|
||||
ipCounts: 0,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
@@ -63,7 +66,15 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getLockedIp()
|
||||
},
|
||||
methods: {
|
||||
getLockedIp() {
|
||||
this.$axios.get('/api/v1/settings/security/block-ip/').then(res => {
|
||||
this.ipCounts = res.count
|
||||
})
|
||||
},
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
136
src/components/Apps/ChangeSecret/RecordViewSecret.vue
Normal file
136
src/components/Apps/ChangeSecret/RecordViewSecret.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:title="title"
|
||||
:visible.sync="showSecret"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
|
||||
<el-form-item :label="$tc('accounts.AccountChangeSecret.OldSecret')">
|
||||
<ShowKeyCopyFormatter
|
||||
:cell-value="secretInfo.old_secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: 'old_secret'
|
||||
}}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('accounts.AccountChangeSecret.NewSecret')">
|
||||
<ShowKeyCopyFormatter
|
||||
:cell-value="secretInfo.new_secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: 'new_secret'
|
||||
}}"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export default {
|
||||
name: 'RecordViewSecret',
|
||||
components: {
|
||||
Dialog,
|
||||
ShowKeyCopyFormatter
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$tc('common.ViewSecret')
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
secretInfo: {},
|
||||
showSecret: false,
|
||||
mfaDialogVisible: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted() {
|
||||
this.showSecretDialog()
|
||||
},
|
||||
methods: {
|
||||
accountConfirmHandle() {
|
||||
this.showSecret = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
showSecretDialog() {
|
||||
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
|
||||
this.secretInfo = res
|
||||
this.showSecret = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-textarea >>> .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
>>> .el-form-item__label {
|
||||
padding-right: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
>>> .el-form-item__content {
|
||||
line-height: 30px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
163
src/components/Apps/ChatAi/components/ChitChat/ChatInput.vue
Normal file
163
src/components/Apps/ChatAi/components/ChitChat/ChatInput.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="chat-action">
|
||||
<Select2
|
||||
v-model="select.value"
|
||||
:disabled="isLoading || isSelectDisabled"
|
||||
v-bind="select"
|
||||
@change="onSelectChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
:disabled="isLoading"
|
||||
:placeholder="$tc('common.InputMessage')"
|
||||
type="textarea"
|
||||
@compositionend="isIM = false"
|
||||
@compositionstart="isIM = true"
|
||||
@keypress.native="onKeyEnter"
|
||||
/>
|
||||
<div class="input-action">
|
||||
<span class="right">
|
||||
<i :class="{'active': inputValue }" class="fa fa-send" @click="onSendHandle" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Select2 from '../../../../Form/FormFields/Select2.vue'
|
||||
import { useChat } from '../../useChat.js'
|
||||
|
||||
const { setLoading } = useChat()
|
||||
|
||||
export default {
|
||||
components: { Select2 },
|
||||
props: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isIM: false,
|
||||
inputValue: '',
|
||||
select: {
|
||||
url: '/api/v1/settings/chatai-prompts/',
|
||||
value: '',
|
||||
multiple: false,
|
||||
placeholder: this.$t('common.Prompt'),
|
||||
ajax: {
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.content }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading
|
||||
}),
|
||||
isSelectDisabled() {
|
||||
return !!this.select.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onKeyEnter(event) {
|
||||
if (event.key === 'Enter') {
|
||||
if ((!this.isIM && !event.shiftKey) || (this.isIM && event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
this.onSendHandle()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSendHandle() {
|
||||
if (!this.inputValue) return
|
||||
|
||||
setLoading(true)
|
||||
this.$emit('send', this.inputValue)
|
||||
this.inputValue = ''
|
||||
},
|
||||
onSelectChange(value) {
|
||||
this.$emit('select-prompt', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
.chat-action {
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
&>>> .el-select {
|
||||
width: 50%;
|
||||
.el-input__inner {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 14px;
|
||||
border-color: transparent;
|
||||
background-color: #f7f7f8;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
.el-input__icon {
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 12px;
|
||||
&:has(.el-textarea__inner:focus) {
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
&>>> .el-textarea {
|
||||
height: 100%;
|
||||
.el-textarea__inner {
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
resize: none;
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-textarea.is-disabled + .input-action {
|
||||
background-color: #F5F7FA;
|
||||
cursor: no-drop;
|
||||
i {
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
.input-action {
|
||||
overflow: hidden;
|
||||
padding: 0 16px 15px;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
.right {
|
||||
float: right;
|
||||
.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
src/components/Apps/ChatAi/components/ChitChat/ChatMessage.vue
Normal file
186
src/components/Apps/ChatAi/components/ChitChat/ChatMessage.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div :class="{'user-role': isUserRole}" class="chat-item">
|
||||
<div class="avatar">
|
||||
<el-avatar :src="isUserRole ? userUrl : chatUrl" class="header-avatar" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="operational">
|
||||
<span class="date">
|
||||
{{ $moment(item.message.create_time).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div class="message-content">
|
||||
<span v-if="isSystemError" class="error">
|
||||
{{ item.message.content }}
|
||||
</span>
|
||||
<span v-else class="chat-text">
|
||||
<MessageText :message="item.message" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-tooltip
|
||||
v-if="isSystemError && isLoading"
|
||||
:content="$tc('common.Reconnect')"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
>
|
||||
<svg-icon icon-class="refresh" @click="onRefresh" />
|
||||
</el-tooltip>
|
||||
<el-dropdown v-else size="small" @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
<i class="fa fa-ellipsis-v" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="i in dropdownOptions" :key="i.action" :command="i.action">
|
||||
{{ i.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageText from './MessageText.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { copy } from '@/utils/common'
|
||||
import { useChat } from '../../useChat.js'
|
||||
import { reconnect } from '@/utils/socket'
|
||||
|
||||
const { setLoading, removeLoadingMessageInChat } = useChat()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageText
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chatUrl: require('@/assets/img/chat.png'),
|
||||
userUrl: '/api/v1/settings/logo/',
|
||||
dropdownOptions: [
|
||||
{
|
||||
action: 'copy',
|
||||
label: this.$t('common.Copy')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading
|
||||
}),
|
||||
isUserRole() {
|
||||
return this.item.message?.role === 'user'
|
||||
},
|
||||
isSystemError() {
|
||||
return this.item.type === 'error' && this.item.message?.role === 'assistant'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onRefresh() {
|
||||
reconnect()
|
||||
removeLoadingMessageInChat()
|
||||
setLoading(false)
|
||||
},
|
||||
handleCommand(value) {
|
||||
if (value === 'copy') {
|
||||
copy(this.item.message.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-item {
|
||||
display: flex;
|
||||
padding: 16px 14px 0;
|
||||
&:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.avatar {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
.header-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&>>> img {
|
||||
background-color: #e5e5e7;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin-left: 6px;
|
||||
overflow: hidden;
|
||||
.operational {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
.copy {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.message {
|
||||
display: -webkit-box;
|
||||
.message-content {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border-radius: 2px 12px 12px;
|
||||
background-color: #f0f1f5;
|
||||
}
|
||||
.action {
|
||||
.svg-icon {
|
||||
transform: translateY(50%);
|
||||
margin-left: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-dropdown {
|
||||
height: 32px;
|
||||
line-height: 37px;
|
||||
font-size: 13px;
|
||||
.el-dropdown-link {
|
||||
i {
|
||||
padding: 4px 5px;
|
||||
font-size: 15px;
|
||||
color: #8d9091;
|
||||
&:hover {
|
||||
color: #7b8085
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-role {
|
||||
flex-direction: row-reverse;
|
||||
.content {
|
||||
margin-right: 10px;
|
||||
.operational {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.message {
|
||||
flex-direction: row-reverse;
|
||||
.message-content {
|
||||
background-color: var(--menu-hover);
|
||||
border-radius: 12px 2px 12px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
178
src/components/Apps/ChatAi/components/ChitChat/MessageText.vue
Normal file
178
src/components/Apps/ChatAi/components/ChitChat/MessageText.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="textRef" class="leading-relaxed break-words">
|
||||
<span v-if="message.content === 'loading'" class="loading-box">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<div v-else class="inline-block markdown-body" v-html="text" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mdKatex from '@traptitech/markdown-it-katex'
|
||||
import mila from 'markdown-it-link-attributes'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import { copy } from '@/utils/common'
|
||||
|
||||
/* eslint-disable vue/no-v-html */
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
markdown: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
const value = this.message?.content || ''
|
||||
if (value && this.markdown) {
|
||||
return this.markdown?.render(value)
|
||||
}
|
||||
return this.$xss.process(value)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
updated() {
|
||||
this.addCopyEvents()
|
||||
},
|
||||
destroyed() {
|
||||
this.removeCopyEvents()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const vm = this
|
||||
this.markdown = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language))
|
||||
if (validLang) {
|
||||
const lang = language || ''
|
||||
return vm.highlightBlock(hljs.highlight(lang, code, true).value, lang)
|
||||
}
|
||||
return vm.highlightBlock(hljs.highlightAuto(code).value, '')
|
||||
}
|
||||
})
|
||||
this.markdown.use(mila, { attrs: { target: '_blank', rel: 'noopener', class: 'link-style' }})
|
||||
this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' })
|
||||
},
|
||||
highlightBlock(str, lang) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${'Copy'}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||
},
|
||||
addCopyEvents() {
|
||||
const copyBtn = document.querySelectorAll('.code-block-header__copy')
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const code = btn.parentElement?.nextElementSibling?.textContent
|
||||
if (code) {
|
||||
copy(code)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
removeCopyEvents() {
|
||||
if (this.$refs.textRef) {
|
||||
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.removeEventListener('click', () => {})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
font-size: 13px;
|
||||
&>>> p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
background: inherit;
|
||||
&>>> pre {
|
||||
padding: 0 0 6px 0;
|
||||
.hljs.code-block-body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
&>>> .code-block-wrapper {
|
||||
background: #1F2329;
|
||||
padding: 2px 6px;
|
||||
margin: 5px 0;
|
||||
|
||||
.code-block-body {
|
||||
padding: 5px 10px 0;
|
||||
};
|
||||
.code-block-header {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
background: #353946;
|
||||
color: #c2d1e1;
|
||||
|
||||
.code-block-header__copy {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #6e747b;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hljs.code-block-body.javascript {
|
||||
.hljs-comment {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>> .link-style {
|
||||
color: #487bf4;
|
||||
&:hover {
|
||||
color: #275ee3;
|
||||
}
|
||||
}
|
||||
.loading-box{
|
||||
margin-left: 6px;
|
||||
}
|
||||
.loading-box span{
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
background: #676A6c;
|
||||
animation: load 1.2s ease infinite;
|
||||
}
|
||||
.loading-box span:last-child{
|
||||
margin-right: 0;
|
||||
}
|
||||
@keyframes load{
|
||||
0%{
|
||||
opacity: 1;
|
||||
}
|
||||
100%{
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-box span:nth-child(1){
|
||||
animation-delay: 0.23s;
|
||||
}
|
||||
.loading-box span:nth-child(2){
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
.loading-box span:nth-child(3){
|
||||
animation-delay: 0.49s;
|
||||
}
|
||||
</style>
|
||||
271
src/components/Apps/ChatAi/components/ChitChat/index.vue
Normal file
271
src/components/Apps/ChatAi/components/ChitChat/index.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<div id="scrollRef" class="chat-list">
|
||||
<div v-if="showIntroduction" class="introduction">
|
||||
<div v-for="(item, index) in introduction" :key="index" class="introduction-item" @click="sendIntroduction(item)">
|
||||
<div class="head">
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
<span class="title">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" />
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<el-button
|
||||
v-if="isLoading && socket && socket.readyState === 1"
|
||||
class="stop"
|
||||
icon="fa fa-stop-circle-o"
|
||||
round
|
||||
size="small"
|
||||
@click="onStopHandle"
|
||||
>{{ $tc('common.Stop') }}</el-button>
|
||||
<ChatInput ref="chatInput" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatInput from './ChatInput.vue'
|
||||
import ChatMessage from './ChatMessage.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { closeWebSocket, createWebSocket, onSend, ws } from '@/utils/socket'
|
||||
import { getInputFocus, useChat } from '../../useChat.js'
|
||||
|
||||
const {
|
||||
setLoading,
|
||||
clearChats,
|
||||
addChatMessageById,
|
||||
addMessageToActiveChat,
|
||||
newChatAndAddMessageById,
|
||||
removeLoadingMessageInChat,
|
||||
updateChaMessageContentById,
|
||||
addTemporaryLoadingToChat
|
||||
} = useChat()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatInput,
|
||||
ChatMessage
|
||||
},
|
||||
props: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {},
|
||||
prompt: '',
|
||||
currentConversationId: '',
|
||||
showIntroduction: false,
|
||||
introduction: [
|
||||
{
|
||||
title: this.$t('common.introduction.ConceptTitle'),
|
||||
content: this.$t('common.introduction.ConceptContent')
|
||||
},
|
||||
{
|
||||
title: this.$t('common.introduction.IdeaTitle'),
|
||||
content: this.$t('common.introduction.IdeaContent')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading,
|
||||
activeChat: state => state.chat.activeChat
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
closeWebSocket()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.initWebSocket()
|
||||
this.initChatMessage()
|
||||
},
|
||||
initWebSocket() {
|
||||
const { NODE_ENV, VUE_APP_KAEL_HOST } = process.env || {}
|
||||
const api = '/kael/chat/system/'
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const path = `${protocol}://${window.location.host}${api}`
|
||||
const index = VUE_APP_KAEL_HOST?.indexOf('://')
|
||||
const localPath = protocol + VUE_APP_KAEL_HOST?.substring(index, VUE_APP_KAEL_HOST?.length) + api
|
||||
const url = NODE_ENV === 'development' ? localPath : path
|
||||
createWebSocket(url, this.onWebSocketMessage)
|
||||
},
|
||||
initChatMessage() {
|
||||
this.prompt = ''
|
||||
this.showIntroduction = true
|
||||
this.currentConversationId = ''
|
||||
this.$refs.chatInput.select.value = ''
|
||||
const chat = {
|
||||
message: {
|
||||
content: this.$t('common.ChatHello'),
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
newChatAndAddMessageById(chat)
|
||||
setLoading(false)
|
||||
},
|
||||
onWebSocketMessage(data) {
|
||||
if (data.type === 'message') {
|
||||
this.onChatMessage(data)
|
||||
}
|
||||
if (data.type === 'error') {
|
||||
this.onSystemMessage(data)
|
||||
}
|
||||
},
|
||||
onChatMessage(data) {
|
||||
if (data.conversation_id) {
|
||||
setLoading(true)
|
||||
removeLoadingMessageInChat()
|
||||
this.currentConversationId = data.conversation_id
|
||||
updateChaMessageContentById(data.message.id, data)
|
||||
}
|
||||
if (data.message?.type === 'finish') {
|
||||
setLoading(false)
|
||||
getInputFocus()
|
||||
}
|
||||
},
|
||||
onSystemMessage(data) {
|
||||
data.message = {
|
||||
content: data.system_message,
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
removeLoadingMessageInChat()
|
||||
addMessageToActiveChat(data)
|
||||
this.socketReadyStateSuccess = false
|
||||
setLoading(true)
|
||||
},
|
||||
onSendHandle(value) {
|
||||
this.showIntroduction = false
|
||||
this.socket = ws || {}
|
||||
if (ws?.readyState === 1) {
|
||||
this.socketReadyStateSuccess = true
|
||||
const chat = {
|
||||
message: {
|
||||
content: value,
|
||||
role: 'user',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
const message = {
|
||||
content: value,
|
||||
prompt: this.prompt,
|
||||
conversation_id: this.currentConversationId || ''
|
||||
}
|
||||
addChatMessageById(chat)
|
||||
onSend(message)
|
||||
addTemporaryLoadingToChat()
|
||||
} else {
|
||||
const chat = {
|
||||
message: {
|
||||
content: this.$t('common.ConnectionDropped'),
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
},
|
||||
type: 'error'
|
||||
}
|
||||
addChatMessageById(chat)
|
||||
this.socketReadyStateSuccess = false
|
||||
setLoading(true)
|
||||
}
|
||||
},
|
||||
onSelectPromptHandle(value) {
|
||||
this.prompt = value
|
||||
this.currentConversationId = ''
|
||||
this.showIntroduction = false
|
||||
this.onSendHandle(value)
|
||||
},
|
||||
onNewChat() {
|
||||
clearChats()
|
||||
this.initChatMessage()
|
||||
},
|
||||
onStopHandle() {
|
||||
this.$axios.post(
|
||||
'/kael/interrupt_current_ask/',
|
||||
{ id: this.currentConversationId || '' }
|
||||
).finally(() => {
|
||||
removeLoadingMessageInChat()
|
||||
setLoading(false)
|
||||
})
|
||||
},
|
||||
sendIntroduction(item) {
|
||||
this.showIntroduction = false
|
||||
this.onSendHandle(item.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.introduction {
|
||||
padding: 16px 14px 0;
|
||||
|
||||
.introduction-item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
background-color: var(--menu-hover);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 2px 2px #00000014;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.head {
|
||||
margin-bottom: 2px;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
color: #373739;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: inline-block;
|
||||
color: #a7a7ab;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: 0 15px 25px;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
.input-box {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid #ececec;
|
||||
}
|
||||
.stop {
|
||||
position: absolute;
|
||||
top: -37px;
|
||||
left: 50%;
|
||||
z-index: 11;
|
||||
transform: translateX(-50%);
|
||||
>>> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/Apps/ChatAi/components/Sidebar/index.vue
Normal file
81
src/components/Apps/ChatAi/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="close-sidebar">
|
||||
<i v-if="hasClose" class="el-icon-close" @click="onClose" />
|
||||
</div>
|
||||
<el-tabs v-model="active" :tab-position="'right'" @tab-click="handleClick">
|
||||
<el-tab-pane v-for="(item) in submenu" :key="item.name" :name="item.name">
|
||||
<span slot="label">
|
||||
<el-tooltip effect="dark" placement="left" :content="item.label">
|
||||
<svg-icon :icon-class="item.icon" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
active: {
|
||||
type: String,
|
||||
default: 'chat'
|
||||
},
|
||||
hasClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
submenu: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(tab, event) {
|
||||
this.$emit('tab-click', tab)
|
||||
},
|
||||
onClose() {
|
||||
this.$parent.onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f1f5;
|
||||
.close-sidebar {
|
||||
height: 48px;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
i {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--menu-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>> .el-tabs {
|
||||
.el-tabs__item {
|
||||
padding: 0 13px;
|
||||
font-size: 15px;
|
||||
:hover {
|
||||
color: #7b8085;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
src/components/Apps/ChatAi/index.vue
Normal file
145
src/components/Apps/ChatAi/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="chat">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<img :src="robotUrl" alt="">
|
||||
<span class="title">{{ title }}</span>
|
||||
</div>
|
||||
<span class="new" @click="onNewChat">
|
||||
<i class="el-icon-plus" />
|
||||
<span>{{ $tc('common.NewChat') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<keep-alive>
|
||||
<component :is="active" ref="component" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<Sidebar v-bind="$attrs" :active.sync="active" :submenu="submenu" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import Chat from './components/ChitChat/index.vue'
|
||||
import { getInputFocus } from './useChat.js'
|
||||
import { ws } from '@/utils/socket'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chat,
|
||||
Sidebar
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('setting.ChatAI')
|
||||
}
|
||||
},
|
||||
drawerPanelVisible: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: 'chat',
|
||||
robotUrl: require('../../../assets/img/robot-assistant.png'),
|
||||
submenu: [
|
||||
{
|
||||
name: 'chat',
|
||||
label: this.$t('common.Chat'),
|
||||
icon: 'chat'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
drawerPanelVisible(value) {
|
||||
if (value && !ws) {
|
||||
this.initWebSocket()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initWebSocket() {
|
||||
this.$refs.component?.init()
|
||||
},
|
||||
onClose() {
|
||||
this.$parent.show = false
|
||||
},
|
||||
onNewChat() {
|
||||
this.active = 'chat'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.component?.onNewChat()
|
||||
getInputFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #ececec;
|
||||
.left {
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
.title {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
.new {
|
||||
display: inline-block;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 16px;
|
||||
padding: 0 10px;
|
||||
transform: translateY(32%);
|
||||
color: var(--color-primary);
|
||||
background-color: #f7f7f8;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
src/components/Apps/ChatAi/useChat.js
Normal file
80
src/components/Apps/ChatAi/useChat.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import store from '@/store'
|
||||
import { pageScroll } from '@/utils/common'
|
||||
|
||||
export const getInputFocus = () => {
|
||||
const dom = document.querySelector('.chat-input .el-textarea__inner')
|
||||
setTimeout(() => dom?.focus(), 200)
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const chatStore = {}
|
||||
|
||||
const setLoading = (loading) => {
|
||||
store.commit('chat/setLoading', loading)
|
||||
}
|
||||
|
||||
const onNewChat = (name) => {
|
||||
const data = {
|
||||
name: name || `new chat`,
|
||||
id: 1,
|
||||
conversation_id: '',
|
||||
chats: []
|
||||
}
|
||||
store.commit('chat/addChatToStore', data)
|
||||
}
|
||||
|
||||
const clearChats = () => {
|
||||
store.commit('chat/clearChats')
|
||||
}
|
||||
|
||||
const addMessageToActiveChat = (chat) => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
}
|
||||
|
||||
const removeLoadingMessageInChat = () => {
|
||||
store.commit('chat/removeLoadingMessageInChat')
|
||||
}
|
||||
|
||||
const addChatMessageById = (chat) => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
if (chat?.conversation_id) {
|
||||
store.commit('chat/setActiveChatConversationId', chat.conversation_id)
|
||||
}
|
||||
pageScroll('scrollRef')
|
||||
}
|
||||
|
||||
const addTemporaryLoadingToChat = () => {
|
||||
const temporaryChat = {
|
||||
message: {
|
||||
content: 'loading',
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
addChatMessageById(temporaryChat)
|
||||
}
|
||||
|
||||
const newChatAndAddMessageById = (chat) => {
|
||||
onNewChat(chat.message.content)
|
||||
addChatMessageById(chat)
|
||||
}
|
||||
|
||||
const updateChaMessageContentById = (id, data) => {
|
||||
store.commit('chat/updateChaMessageContentById', { id, data })
|
||||
pageScroll('scrollRef')
|
||||
}
|
||||
|
||||
return {
|
||||
chatStore,
|
||||
setLoading,
|
||||
onNewChat,
|
||||
clearChats,
|
||||
getInputFocus,
|
||||
addMessageToActiveChat,
|
||||
newChatAndAddMessageById,
|
||||
removeLoadingMessageInChat,
|
||||
addChatMessageById,
|
||||
addTemporaryLoadingToChat,
|
||||
updateChaMessageContentById
|
||||
}
|
||||
}
|
||||
209
src/components/Apps/DrawerPanel/index.vue
Normal file
209
src/components/Apps/DrawerPanel/index.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div ref="drawer" :class="{show: show}" class="drawer">
|
||||
<div :style="{'background-color': modal ? 'rgba(0, 0, 0, .3)' : 'transparent'}" class="modal" />
|
||||
<div :style="{'width': width}" class="drawer-panel">
|
||||
<div v-show="!show" ref="dragBox" class="handle-button">
|
||||
<i v-if="icon.startsWith('fa') || icon.startsWith('el')" :class="show ? 'el-icon-close': icon" />
|
||||
<img v-else :src="icon" alt="">
|
||||
</div>
|
||||
<div class="drawer-panel-item">
|
||||
<slot :drawer-panel-visible="show" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DrawerPanel',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'el-icon-setting'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '440px'
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
clickNotClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(value) {
|
||||
if (value && !this.clickNotClose) {
|
||||
this.addEventClick()
|
||||
}
|
||||
this.$emit('toggle', this.show)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.insertToBody()
|
||||
},
|
||||
beforeDestroy() {
|
||||
const element = this.$refs.drawer
|
||||
element.remove()
|
||||
window.removeEventListener('click', this.closeSidebar)
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
const dragBox = this.$refs.dragBox
|
||||
const clientOffset = {}
|
||||
dragBox.addEventListener('mousedown', (event) => {
|
||||
const offsetX = dragBox.getBoundingClientRect().left
|
||||
const offsetY = dragBox.getBoundingClientRect().top
|
||||
const innerX = event.clientX - offsetX
|
||||
const innerY = event.clientY - offsetY
|
||||
|
||||
clientOffset.clientX = event.clientX
|
||||
clientOffset.clientY = event.clientY
|
||||
document.onmousemove = function(event) {
|
||||
dragBox.style.left = event.clientX - innerX + 'px'
|
||||
dragBox.style.top = event.clientY - innerY + 'px'
|
||||
const dragDivTop = window.innerHeight - dragBox.getBoundingClientRect().height
|
||||
const dragDivLeft = window.innerWidth - dragBox.getBoundingClientRect().width
|
||||
dragBox.style.left = dragDivLeft + 'px'
|
||||
dragBox.style.left = '-48px'
|
||||
if (dragBox.getBoundingClientRect().top <= 0) {
|
||||
dragBox.style.top = '0px'
|
||||
}
|
||||
if (dragBox.getBoundingClientRect().top >= dragDivTop) {
|
||||
dragBox.style.top = dragDivTop + 'px'
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
document.onmouseup = function() {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}, false)
|
||||
dragBox.addEventListener('mouseup', (event) => {
|
||||
const clientX = event.clientX
|
||||
const clientY = event.clientY
|
||||
if (this.isDifferenceWithinThreshold(clientX, clientOffset.clientX) && this.isDifferenceWithinThreshold(clientY, clientOffset.clientY)) {
|
||||
this.show = !this.show
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
isDifferenceWithinThreshold(num1, num2, threshold = 5) {
|
||||
const difference = Math.abs(num1 - num2)
|
||||
return difference <= threshold
|
||||
},
|
||||
addEventClick() {
|
||||
window.addEventListener('click', this.closeSidebar)
|
||||
},
|
||||
closeSidebar(evt) {
|
||||
const parent = evt.target.closest('.drawer-panel')
|
||||
if (!parent && evt.target.className === 'modal') {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
insertToBody() {
|
||||
const element = this.$refs.drawer
|
||||
const body = document.querySelector('body')
|
||||
body.insertBefore(element, body.firstChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
|
||||
background: rgba(0, 0, 0, .3);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.drawer-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
min-width: 260px;
|
||||
height: 100vh;
|
||||
user-select: none;
|
||||
transition: transform .25s cubic-bezier(.7, .3, .1, 1);
|
||||
box-shadow: 0 0 8px 4px #00000014;
|
||||
transform: translate(100%);
|
||||
background: #FFFFFF;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.drawer-panel-item {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drawer-panel-item::-webkit-scrollbar-track {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all .3s cubic-bezier(.7, .3, .1, 1);
|
||||
}
|
||||
|
||||
.show .modal {
|
||||
z-index: 1003;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.show .drawer-panel {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
.handle-button {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: -48px;
|
||||
width: 48px;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
color: #fff;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 8px 4px #00000014;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
left: -50px !important;
|
||||
width: 50px !important;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
i {
|
||||
font-size: 20px;
|
||||
line-height: 45px;
|
||||
}
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
transform: translateY(10%);
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,8 @@
|
||||
<script type="text/jsx">
|
||||
import TreeTable from '../../Table/TreeTable/index.vue'
|
||||
import { DetailFormatter } from '@/components/Table/TableFormatters'
|
||||
import { AccountInfoFormatter } from '@/components/Table/TableFormatters'
|
||||
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
|
||||
|
||||
export default {
|
||||
name: 'GrantedAssets',
|
||||
@@ -49,6 +51,11 @@ export default {
|
||||
url: this.tableUrl,
|
||||
// ?assets=0不显示资产. =1显示资产
|
||||
treeUrl: this.treeUrl,
|
||||
edit: {
|
||||
drag: {
|
||||
isMove: false
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onSelected: (event, node) => vm.onSelected(node, vm),
|
||||
refresh: vm.refreshObjectAssetPermission
|
||||
@@ -57,10 +64,11 @@ export default {
|
||||
tableConfig: {
|
||||
url: this.tableUrl,
|
||||
hasTree: true,
|
||||
columnsExtra: ['view_account'],
|
||||
columnsExclude: ['spec_info'],
|
||||
columnShow: {
|
||||
columnsShow: {
|
||||
min: ['name', 'address', 'accounts'],
|
||||
default: ['name', 'address', 'accounts', 'actions']
|
||||
default: ['name', 'address', 'platform', 'view_account', 'connectivity']
|
||||
},
|
||||
columnsMeta: {
|
||||
name: {
|
||||
@@ -71,7 +79,13 @@ export default {
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
},
|
||||
view_account: {
|
||||
label: this.$t('assets.Account'),
|
||||
formatter: AccountInfoFormatter,
|
||||
width: '100px'
|
||||
},
|
||||
connectivity: connectivityMeta
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
:width="'36%'"
|
||||
class="dialog-content"
|
||||
v-bind="$attrs"
|
||||
width="600px"
|
||||
@confirm="visible = false"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div v-if="ConfirmType === 'relogin'">
|
||||
<div v-if="confirmTypeRequired === 'relogin'">
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
:title="$tc('auth.ReLoginTitle')"
|
||||
center
|
||||
style="margin-bottom: 20px;"
|
||||
type="info"
|
||||
type="error"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button
|
||||
size="mini"
|
||||
style="width: 100%; line-height:20px;"
|
||||
type="primary"
|
||||
@click="logOut"
|
||||
>
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
|
||||
{{ this.$t('auth.ReLogin') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
@@ -39,14 +35,13 @@
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" :span="24" class="add">
|
||||
<el-select
|
||||
v-model="Select"
|
||||
:disabled="ConfirmType === 'password'"
|
||||
v-model="subTypeSelected"
|
||||
style="width: 100%; margin-bottom: 20px;"
|
||||
@change="helpText(Select)"
|
||||
@change="handleSubTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, i) of Content"
|
||||
:key="i"
|
||||
v-for="item of subTypeChoices"
|
||||
:key="item.name"
|
||||
:disabled="item.disabled"
|
||||
:label="item.display_name"
|
||||
:value="item.name"
|
||||
@@ -56,28 +51,28 @@
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" style="display: flex; margin-bottom: 20px;">
|
||||
<el-input v-model="SecretKey" :placeholder="HelpText" :show-password="showPassword" />
|
||||
<span v-if="Select === 'sms'" style="margin: -1px 0 0 20px;">
|
||||
<el-input
|
||||
v-model="secretValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:show-password="showPassword"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
|
||||
<el-button
|
||||
:disabled="smsBtndisabled"
|
||||
:disabled="smsBtnDisabled"
|
||||
size="mini"
|
||||
style="line-height:20px; float: right;"
|
||||
type="primary"
|
||||
@click="sendChallengeCode"
|
||||
@click="sendSMSCode"
|
||||
>
|
||||
{{ smsBtnText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-row :gutter="24" style="margin: 10px auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button
|
||||
size="mini"
|
||||
style="width: 100%; line-height:20px;"
|
||||
type="primary"
|
||||
@click="userConfirm"
|
||||
>
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="handleConfirm">
|
||||
{{ this.$t('common.Confirm') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
@@ -87,6 +82,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'UserConfirmDialog',
|
||||
@@ -96,125 +92,120 @@ export default {
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
default: ''
|
||||
},
|
||||
handler: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
title: this.$t('common.CurrentUserVerify'),
|
||||
smsWidth: 0,
|
||||
Select: '',
|
||||
Level: null,
|
||||
HelpText: '',
|
||||
smsBtnText: '',
|
||||
smsBtndisabled: false,
|
||||
ConfirmType: '',
|
||||
Content: null,
|
||||
SecretKey: '',
|
||||
visible: false
|
||||
subTypeSelected: '',
|
||||
inputPlaceholder: '',
|
||||
smsBtnText: this.$t('common.SendVerificationCode'),
|
||||
smsBtnDisabled: false,
|
||||
confirmTypeRequired: '',
|
||||
subTypeChoices: [],
|
||||
secretValue: '',
|
||||
visible: false,
|
||||
callback: null,
|
||||
cancel: null,
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showPassword() {
|
||||
if (this.ConfirmType === 'password') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.$emit('UserConfirmCancel', true)
|
||||
}
|
||||
return this.confirmTypeRequired === 'password'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.smsBtnText = this.$t('common.SendVerificationCode')
|
||||
this.$axios.get(`${this.url}`, { disableFlashErrorMsg: true }).then(
|
||||
() => { this.$emit('UserConfirmDone', true) }).catch((err) => {
|
||||
const confirm_type = err.response.data.code
|
||||
this.$axios.get('/api/v1/authentication/confirm/', { params: { confirm_type: confirm_type }}).then((data) => {
|
||||
this.ConfirmType = data.confirm_type
|
||||
this.Content = data.content
|
||||
if (this.ConfirmType === 'relogin') {
|
||||
this.$axios.post(
|
||||
`/api/v1/authentication/confirm/`,
|
||||
{
|
||||
confirm_type: this.ConfirmType,
|
||||
secret_key: ''
|
||||
},
|
||||
{ disableFlashErrorMsg: true },
|
||||
).then(() => { this.$emit('UserConfirmDone', true) }).catch(() => {
|
||||
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('showConfirmDialog', this.performConfirm)
|
||||
},
|
||||
methods: {
|
||||
handleSubTypeChange(val) {
|
||||
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
|
||||
this.smsWidth = val === 'sms' ? 6 : 0
|
||||
},
|
||||
performConfirm: _.throttle(function({ response, callback, cancel }) {
|
||||
if (this.processing || this.visible) {
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
this.callback = callback
|
||||
this.cancel = cancel
|
||||
this.$log.debug('perform confirm action')
|
||||
const confirmType = response.data?.code
|
||||
const confirmUrl = '/api/v1/authentication/confirm/'
|
||||
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType }}).then((data) => {
|
||||
this.confirmTypeRequired = data.confirm_type
|
||||
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
this.$axios.post(confirmUrl, { 'confirm_type': 'relogin', 'secret_key': 'x' }).then(() => {
|
||||
this.callback()
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.title = this.$t('auth.NeedReLogin')
|
||||
this.visible = true
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.ConfirmType === 'mfa') {
|
||||
this.Select = this.Content.filter(item => !item.disabled)[0].name
|
||||
if (this.Select === 'sms') {
|
||||
this.smsWidth = 6
|
||||
}
|
||||
this.HelpText = this.Content.filter(item => !item.disabled)[0].placeholder
|
||||
} else if (this.ConfirmType === 'password') {
|
||||
this.Select = this.$t('setting.password')
|
||||
this.HelpText = this.$t('common.PasswordRequireForSecurity')
|
||||
this.Content = [{ 'name': 'password' }]
|
||||
}
|
||||
this.title = this.$t('common.CurrentUserVerify')
|
||||
this.subTypeChoices = data.content
|
||||
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
|
||||
this.subTypeSelected = defaultSubType.name
|
||||
this.inputPlaceholder = defaultSubType.placeholder
|
||||
this.visible = true
|
||||
}).catch(() => {
|
||||
this.$emit('AuthMFAError', true)
|
||||
}).catch((err) => {
|
||||
const data = err.response?.data
|
||||
const msg = data?.error || data?.detail || data?.msg || this.$t('common.GetConfirmTypeFailed')
|
||||
this.$message.error(msg)
|
||||
this.cancel(err)
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
helpText(val) {
|
||||
this.HelpText = this.Content.filter(item => item.name === val)[0]?.placeholder
|
||||
if (val === 'sms') {
|
||||
this.smsWidth = 6
|
||||
} else {
|
||||
this.smsWidth = 0
|
||||
}
|
||||
},
|
||||
logOut() {
|
||||
}, 300),
|
||||
logout() {
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
},
|
||||
sendChallengeCode() {
|
||||
this.$axios.post(
|
||||
`/api/v1/authentication/mfa/select/`, {
|
||||
type: 'sms'
|
||||
}
|
||||
).then(res => {
|
||||
this.$message.success(this.$t('common.VerificationCodeSent'))
|
||||
sendSMSCode() {
|
||||
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: 'sms' }).then(res => {
|
||||
this.$message.success(this.$tc('common.VerificationCodeSent'))
|
||||
let time = 60
|
||||
const interval = setInterval(() => {
|
||||
const originText = this.smsBtnText
|
||||
this.smsBtnText = this.$t('common.Pending') + `: ${time}`
|
||||
this.smsBtndisabled = true
|
||||
this.smsBtnDisabled = true
|
||||
time -= 1
|
||||
|
||||
if (time === 0) {
|
||||
this.smsBtnText = this.$t('common.SendVerificationCode')
|
||||
this.smsBtndisabled = false
|
||||
this.smsBtnText = originText
|
||||
this.smsBtnDisabled = false
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
userConfirm() {
|
||||
if (this.Select === 'otp' && this.SecretKey.length !== 6) {
|
||||
return this.$message.error(this.$t('common.MFAErrorMsg'))
|
||||
handleConfirm() {
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
return this.logout()
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/authentication/confirm/`, {
|
||||
confirm_type: this.ConfirmType,
|
||||
mfa_type: this.ConfirmType === 'password' ? undefined : this.Select,
|
||||
secret_key: this.SecretKey
|
||||
}
|
||||
).then(res => {
|
||||
this.$emit('UserConfirmDone', true)
|
||||
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
|
||||
return this.$message.error(this.$tc('common.MFAErrorMsg'))
|
||||
}
|
||||
const data = {
|
||||
confirm_type: this.confirmTypeRequired,
|
||||
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
|
||||
secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue
|
||||
}
|
||||
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
|
||||
this.callback()
|
||||
this.secretValue = ''
|
||||
this.visible = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -228,5 +219,16 @@ export default {
|
||||
|
||||
.dialog-content >>> .el-dialog {
|
||||
padding: 8px;
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,9 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
displayValue() {
|
||||
if ([null, undefined, ''].includes(this.value)) {
|
||||
@@ -64,8 +67,19 @@ export default {
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
let formatterData = ''
|
||||
if (typeof this.formatter === 'function') {
|
||||
return this.formatter(this.item, this.value)
|
||||
const data = this.formatter(this.item, this.value)
|
||||
if (data instanceof Promise) {
|
||||
data.then(res => {
|
||||
formatterData = res
|
||||
})
|
||||
} else {
|
||||
formatterData = data
|
||||
}
|
||||
return (
|
||||
<span style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: '1.2' }}>{formatterData}</span>
|
||||
)
|
||||
}
|
||||
if (this.value instanceof Array) {
|
||||
const newArr = this.value || []
|
||||
|
||||
@@ -106,16 +106,28 @@ export default {
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (typeof value[0] === 'object') {
|
||||
value.forEach(item => {
|
||||
const fieldName = `${name}.${item.name}`
|
||||
if (excludes.includes(fieldName)) {
|
||||
return
|
||||
}
|
||||
this.items.push({
|
||||
key: item.label,
|
||||
value: item.value
|
||||
const firstValue = value[0]
|
||||
if (firstValue.hasOwnProperty('name')) {
|
||||
value.forEach(item => {
|
||||
const fieldName = `${name}.${item.name}`
|
||||
if (excludes.includes(fieldName)) {
|
||||
return
|
||||
}
|
||||
this.items.push({
|
||||
key: item.label,
|
||||
value: item.value
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
value.forEach((item, index) => {
|
||||
const v = Object.entries(item).map(([key, value]) => `${key}:${value}`).join(', ')
|
||||
const data = { value: v }
|
||||
if (index === 0) {
|
||||
data['key'] = label
|
||||
}
|
||||
this.items.push(data)
|
||||
})
|
||||
}
|
||||
} else if (typeof value[0] === 'string') {
|
||||
value.forEach((item, index) => {
|
||||
let data = {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<IBox :fa="fa" :title="title">
|
||||
<el-form class="content" label-position="left" label-width="25%">
|
||||
<el-form-item v-for="item in items" :key="item.key" :label="item.key">
|
||||
<el-form-item v-for="item in iItems" :key="item.key" :label="item.key">
|
||||
<ItemValue :value="item.value" class="item-value" v-bind="item" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -35,6 +35,13 @@ export default {
|
||||
type: String,
|
||||
default: 'left'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iItems: this.items.filter(item => {
|
||||
return !item.hasOwnProperty('has') || item.has === true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -103,6 +103,10 @@ export default {
|
||||
type: Function,
|
||||
default: (obj, that) => {}
|
||||
},
|
||||
allowCreate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onDeleteSuccess: {
|
||||
type: Function,
|
||||
default(obj, that) {
|
||||
@@ -146,6 +150,10 @@ export default {
|
||||
that.$refs.select2.clearSelected()
|
||||
that.$message.success(that.$t('common.AddSuccessMsg'))
|
||||
}
|
||||
},
|
||||
getHasObjects: {
|
||||
type: Function,
|
||||
default: null // (objectIds) => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -163,7 +171,8 @@ export default {
|
||||
options: this.objects,
|
||||
value: this.value,
|
||||
disabled: this.disabled,
|
||||
disabledValues: []
|
||||
disabledValues: [],
|
||||
allowCreate: this.allowCreate
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -258,9 +267,15 @@ export default {
|
||||
return
|
||||
}
|
||||
this.select2.disabledValues = this.hasObjectsId
|
||||
const resp = await createSourceIdCache(this.hasObjectsId)
|
||||
this.params.spm = resp.spm
|
||||
await this.loadHasObjects()
|
||||
if (this.getHasObjects) {
|
||||
this.getHasObjects(this.hasObjectsId).then((data) => {
|
||||
this.iHasObjects = data
|
||||
})
|
||||
} else {
|
||||
const resp = await createSourceIdCache(this.hasObjectsId)
|
||||
this.params.spm = resp.spm
|
||||
await this.loadHasObjects()
|
||||
}
|
||||
},
|
||||
removeObject(obj) {
|
||||
this.performDelete(obj, this).then(
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot />
|
||||
<div v-loading="loadingStatus">
|
||||
<slot />
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<slot name="footer">
|
||||
<el-button v-if="showCancel && showButtons" @click="onCancel">{{ cancelTitle }}</el-button>
|
||||
<el-button v-if="showConfirm && showButtons" :loading="loadingStatus" type="primary" @click="onConfirm">
|
||||
<el-button v-if="showConfirm && showButtons" :disabled="loadingStatus" type="primary" @click="onConfirm">
|
||||
{{ confirmTitle }}
|
||||
</el-button>
|
||||
</slot>
|
||||
@@ -71,13 +73,16 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iWidth() {
|
||||
return this.$store.getters.isMobile ? '80%' : this.width
|
||||
return this.$store.getters.isMobile ? '1000px' : this.width
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancel')
|
||||
@@ -92,7 +97,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.dialog >>> .el-dialog {
|
||||
border-radius: 0.3em;
|
||||
max-width: 1500px;
|
||||
max-width: min(100vw, 1500px);
|
||||
|
||||
.el-icon-circle-check {
|
||||
display: none;
|
||||
@@ -119,9 +124,14 @@ export default {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.dialog >>> .el-dialog {
|
||||
max-width: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
.dialog-footer >>> button.el-button {
|
||||
font-size: 13px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Vue from 'vue'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import ObjectSelect2 from '@/components/Form/FormFields/NestedObjectSelect2.vue'
|
||||
import NestedField from '@/components/Form/AutoDataForm/components/NestedField.vue'
|
||||
import Switcher from '@/components/Form/FormFields/Switcher.vue'
|
||||
@@ -45,13 +44,14 @@ export class FormFieldGenerator {
|
||||
break
|
||||
case 'field':
|
||||
type = ''
|
||||
field.component = Select2
|
||||
field.component = ObjectSelect2
|
||||
if (fieldRemoteMeta.required) {
|
||||
field.el.clearable = false
|
||||
}
|
||||
if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
|
||||
field.component = ObjectSelect2
|
||||
}
|
||||
field.el.label = field.label
|
||||
// if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
|
||||
// field.component = ObjectSelect2
|
||||
// }
|
||||
break
|
||||
case 'string':
|
||||
type = 'input'
|
||||
@@ -76,6 +76,7 @@ export class FormFieldGenerator {
|
||||
break
|
||||
case 'm2m_related_field':
|
||||
field.component = ObjectSelect2
|
||||
field.el.label = field.label
|
||||
break
|
||||
case 'nested object':
|
||||
type = 'nestedField'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')">
|
||||
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')" class="crontab-panel">
|
||||
<CrontabMin
|
||||
ref="cronmin"
|
||||
:check="checkNumber"
|
||||
@@ -59,38 +59,38 @@
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.min"
|
||||
min="0"
|
||||
max="5"
|
||||
size="small"
|
||||
min="0"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.hour"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.day"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.month"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.week"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -100,7 +100,7 @@
|
||||
<div style="font-size: 13px;">{{ contabValueString }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<CrontabResult :ex="contabValueString" />
|
||||
<CrontabResult :ex="contabValueString" @crontabDiffChange="crontabDiffChangeHandle" />
|
||||
|
||||
<div class="pop_btn">
|
||||
<el-button
|
||||
@@ -130,7 +130,7 @@ import CrontabWeek from './components/Crontab-Week.vue'
|
||||
import CrontabResult from './components/Crontab-Result.vue'
|
||||
|
||||
export default {
|
||||
name: 'Vcrontab',
|
||||
name: 'VCrontab',
|
||||
components: {
|
||||
CrontabMin,
|
||||
CrontabHour,
|
||||
@@ -167,7 +167,8 @@ export default {
|
||||
week: '*'
|
||||
// year: "",
|
||||
},
|
||||
newContabValueString: ''
|
||||
newContabValueString: '',
|
||||
crontabDiff: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -364,6 +365,12 @@ export default {
|
||||
},
|
||||
// 填充表达式
|
||||
submitFill() {
|
||||
const crontabDiffMin = this.crontabDiff / 1000 / 60
|
||||
if (crontabDiffMin > 0 && crontabDiffMin < 10) {
|
||||
const msg = this.$tc('common.crontabDiffError')
|
||||
this.$message.error(msg)
|
||||
return
|
||||
}
|
||||
this.$emit('fill', this.contabValueString)
|
||||
this.hidePopup()
|
||||
},
|
||||
@@ -381,13 +388,16 @@ export default {
|
||||
for (const j in this.contabValueObj) {
|
||||
this.changeRadio(j, this.contabValueObj[j])
|
||||
}
|
||||
},
|
||||
crontabDiffChangeHandle(diff) {
|
||||
this.crontabDiff = diff
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
<style lang='scss' scoped>
|
||||
.pop_btn {
|
||||
text-align: center;
|
||||
float: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@@ -453,6 +463,12 @@ export default {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.crontab-panel {
|
||||
> > > .el-input-number {
|
||||
margin: 0 5px
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item--mini.el-form-item,
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="31" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="31" /> {{ this.$t('common.CronTab.day') }}
|
||||
<el-input-number v-model="cycle01" :max="31" :min="0" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="31" :min="0" size="mini" /> {{ this.$t('common.CronTab.day') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="31" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :max="31" :min="1" size="mini" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
clearable
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.hour') }}
|
||||
<el-input-number v-model="cycle01" :max="60" :min="0" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="60" :min="0" size="mini" /> {{ this.$t('common.CronTab.hour') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
clearable
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
|
||||
@@ -7,18 +7,11 @@
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.min') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" />
|
||||
{{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -27,13 +20,13 @@
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
clearable
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
size="small"
|
||||
style="width:100%"
|
||||
>
|
||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item-1 }}</el-option>
|
||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
@@ -158,7 +151,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :min="1" :max="12" /> -
|
||||
<el-input-number v-model="cycle02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}
|
||||
<el-input-number v-model="cycle01" :max="12" :min="1" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
clearable
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<script>
|
||||
import parser from 'cron-parser'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'CrontabResult',
|
||||
props: {
|
||||
@@ -51,6 +52,10 @@ export default {
|
||||
const cur = interval.next().toString()
|
||||
this.resultList.push(toSafeLocalDateStr(cur))
|
||||
}
|
||||
const first = new Date(this.resultList[0])
|
||||
const second = new Date(this.resultList[1])
|
||||
const diff = Math.abs(second - first)
|
||||
this.$emit('crontabDiffChange', diff)
|
||||
} catch (error) {
|
||||
this.isShow = false
|
||||
// debug(error, 'error')
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.cycleFromWeek') }}
|
||||
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
|
||||
<el-input-number v-model="cycle02" :min="1" :max="7" />
|
||||
<el-input-number v-model="cycle01" :max="7" :min="1" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="7" :min="1" size="mini" />
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
clearable
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="box">
|
||||
<el-input v-model="input" clearable @focus="showDialog" @clear="onClear" />
|
||||
<el-input v-model="input" clearable @clear="onClear" @focus="showDialog" />
|
||||
</div>
|
||||
<el-dialog :title="$tc('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
|
||||
<el-dialog
|
||||
:title="$tc('common.CronTab.newCron')"
|
||||
:visible.sync="showCron"
|
||||
append-to-body
|
||||
top="8vh"
|
||||
width="650px"
|
||||
>
|
||||
<Crontab
|
||||
:expression="expression"
|
||||
@hide="showCron = false"
|
||||
@fill="crontabFill"
|
||||
@hide="showCron = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
v-bind="data.attrs"
|
||||
>
|
||||
<template v-if="data.helpTips" #label>
|
||||
<el-tooltip placement="bottom" effect="light" popper-class="help-tips">
|
||||
<div slot="content" v-html="data.helpTips" />
|
||||
<el-button style="padding: 0">
|
||||
<i class="fa fa-info-circle" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
{{ data.label }}
|
||||
<el-tooltip placement="top" effect="light" popper-class="help-tips">
|
||||
<div slot="content" v-html="data.helpTips" />
|
||||
<i class="fa fa-question-circle-o" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-if="readonly && hasReadonlyContent">
|
||||
<div
|
||||
@@ -70,7 +68,8 @@
|
||||
:key="opt.label"
|
||||
v-bind="opt"
|
||||
:label="'value' in opt ? opt.value : opt.label"
|
||||
>{{ opt.label }}</el-radio>
|
||||
>{{ opt.label }}
|
||||
</el-radio>
|
||||
</template>
|
||||
</custom-component>
|
||||
<div v-if="data.helpText" class="help-block" v-html="data.helpText" />
|
||||
|
||||
@@ -53,13 +53,27 @@ export const matchAlphanumericUnderscore = {
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
// 不能包含()
|
||||
export const MatchExcludeParenthesis = {
|
||||
validator: (rule, value, callback) => {
|
||||
value = value?.trim()
|
||||
if (!/^[^()]*$/.test(value)) {
|
||||
callback(new Error(i18n.t('common.notParenthesis')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
export default {
|
||||
IpCheck,
|
||||
Required,
|
||||
RequiredChange,
|
||||
EmailCheck,
|
||||
specialEmojiCheck,
|
||||
matchAlphanumericUnderscore
|
||||
matchAlphanumericUnderscore,
|
||||
MatchExcludeParenthesis
|
||||
}
|
||||
|
||||
export const JsonRequired = {
|
||||
|
||||
76
src/components/Form/FormFields/AllOrSpec.vue
Normal file
76
src/components/Form/FormFields/AllOrSpec.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-radio-group v-model="type" @input="handleTypeChange">
|
||||
<el-radio v-for="tp of types" :key="tp.name" :label="tp.name">
|
||||
{{ tp.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<Select2 v-if="type === 'spec'" v-model="selected" v-bind="select2" @change="onChangeEmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
|
||||
export default {
|
||||
name: 'AllOrSpec',
|
||||
components: { Select2 },
|
||||
props: {
|
||||
value: {
|
||||
type: [Array],
|
||||
default: () => ([])
|
||||
},
|
||||
select2: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
resource: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'all', // all, selected
|
||||
types: [
|
||||
{ name: 'all', label: this.$t('common.All') },
|
||||
{ name: 'spec', label: this.$t('common.Spec') + this.$t('common.WordSep') + this.resource }
|
||||
],
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iValue() {
|
||||
if (this.type === 'all') {
|
||||
return ['all']
|
||||
} else {
|
||||
return this.selected
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
if (!this.value || this.value.length === 0) {
|
||||
return
|
||||
}
|
||||
if (this.value.indexOf('all') > -1) {
|
||||
this.type = 'all'
|
||||
} else {
|
||||
this.type = 'spec'
|
||||
this.selected = this.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChangeEmit() {
|
||||
this.$emit('input', this.iValue)
|
||||
},
|
||||
handleTypeChange() {
|
||||
this.$emit('input', this.iValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
<span>{label} </span>
|
||||
{helpText
|
||||
? (<el-tooltip content={helpText} placement='top'>
|
||||
<i class='fa fa-info-circle'></i>
|
||||
<i class='fa fa-question-circle-o'></i>
|
||||
</el-tooltip>) : ''}
|
||||
</span>)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="code-editor" style="font-size: 12px">
|
||||
<div class="toolbar">
|
||||
<div
|
||||
v-for="(item,index) in toolbar.left"
|
||||
v-for="(item,index) in iActions"
|
||||
:key="index"
|
||||
style="display: inline-block; margin: 0 2px"
|
||||
>
|
||||
@@ -16,9 +16,8 @@
|
||||
>
|
||||
<i :class="item.icon" style="margin-right: 4px;" />{{ item.name }}
|
||||
</el-button>
|
||||
|
||||
<el-autocomplete
|
||||
v-if="item.type === 'input' && item.el.autoComplete"
|
||||
v-if="item.type === 'input' &&item.el && item.el.autoComplete"
|
||||
v-model="item.value"
|
||||
:placeholder="item.placeholder"
|
||||
:fetch-suggestions="item.el.query"
|
||||
@@ -27,7 +26,14 @@
|
||||
@select="item.callback(item.value)"
|
||||
@change="item.callback(item.value)"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-else-if="item.type==='input'"
|
||||
v-model="item.value"
|
||||
:placeholder="item.placeholder"
|
||||
class="inline-input"
|
||||
size="mini"
|
||||
@change="item.callback(item.value)"
|
||||
/>
|
||||
<div v-if="item.type==='select' && item.el && item.el.create" class="select-content">
|
||||
<span class="filter-label">
|
||||
{{ item.name }}:
|
||||
@@ -87,6 +93,16 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="toolbar.hasOwnProperty('fold')" class="fold">
|
||||
<el-tooltip :content="$tc('common.MoreActions')" placement="top">
|
||||
<i
|
||||
class="fa"
|
||||
:class="[isFold ? 'fa-angle-double-right': 'fa-angle-double-down']"
|
||||
@click="onChangeFold"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="right-side" style="float: right">
|
||||
<div
|
||||
v-for="(item,index) in toolbar.right"
|
||||
@@ -148,9 +164,19 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
isFold: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iActions() {
|
||||
let actions = this.toolbar.left || {}
|
||||
const fold = this.toolbar.fold || {}
|
||||
if (!this.isFold) {
|
||||
actions = { ...actions, ...fold }
|
||||
}
|
||||
return actions
|
||||
},
|
||||
iValue: {
|
||||
get() {
|
||||
return this.value
|
||||
@@ -173,6 +199,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChangeFold() {
|
||||
this.isFold = !this.isFold
|
||||
},
|
||||
getLabel(value, items) {
|
||||
for (const item of items) {
|
||||
if (item.value === value) {
|
||||
@@ -199,6 +228,16 @@ export default {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
i {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
> > > .CodeMirror pre.CodeMirror-line,
|
||||
> > > .CodeMirror-linenumber.CodeMirror-gutter-elt {
|
||||
line-height: 18px !important;
|
||||
|
||||
@@ -88,15 +88,15 @@ export default {
|
||||
getType() {
|
||||
const attrType = this.attr.type || 'str'
|
||||
this.$log.debug('Value field attr type: ', attrType, this.attr, this.match)
|
||||
if (attrType === 'm2m') {
|
||||
if (['m2m', 'fk', 'select'].includes(attrType)) {
|
||||
return 'select'
|
||||
} else if (attrType === 'bool') {
|
||||
return 'bool'
|
||||
} else if (attrType === 'select') {
|
||||
return 'select'
|
||||
}
|
||||
if (['in', 'ip_in'].includes(this.match)) {
|
||||
return 'array'
|
||||
} else if (this.match.startsWith('m2m')) {
|
||||
return 'select'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ export default {
|
||||
id: 'symbol',
|
||||
label: this.$t('common.SpecialSymbol'),
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
id: 'exclude_symbols',
|
||||
label: this.$t('common.ExcludeSymbol'),
|
||||
type: 'input'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'PhoneInput',
|
||||
@@ -35,21 +36,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rawValue: {},
|
||||
countries: [
|
||||
{ name: 'China(中国)', value: '+86' },
|
||||
{ name: 'HongKong(中国香港)', value: '+852' },
|
||||
{ name: 'Macao(中国澳门)', value: '+853' },
|
||||
{ name: 'Taiwan(中国台湾)', value: '+886' },
|
||||
{ name: 'America(America)', value: '+1' },
|
||||
{ name: 'Russia(Россия)', value: '+7' },
|
||||
{ name: 'France(français)', value: '+33' },
|
||||
{ name: 'Britain(Britain)', value: '+44' },
|
||||
{ name: 'Germany(Deutschland)', value: '+49' },
|
||||
{ name: 'Japan(日本)', value: '+81' },
|
||||
{ name: 'Korea(한국)', value: '+82' },
|
||||
{ name: 'India(भारत)', value: '+91' }
|
||||
]
|
||||
rawValue: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -58,7 +45,13 @@ export default {
|
||||
return ''
|
||||
}
|
||||
return `${this.rawValue.code}${this.rawValue.phone}`
|
||||
}
|
||||
},
|
||||
countries: {
|
||||
get() {
|
||||
return this.publicSettings.COUNTRY_CALLING_CODES
|
||||
}
|
||||
},
|
||||
...mapGetters(['publicSettings'])
|
||||
},
|
||||
mounted() {
|
||||
this.rawValue = this.value || { code: '+86', phone: '' }
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:title="$tc('assets.PlatformProtocolConfig') + ':' + protocol.name"
|
||||
class="setting-dialog"
|
||||
v-bind="$attrs"
|
||||
width="70%"
|
||||
width="800px"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-alert v-if="disabled && platformDetail" style="margin-bottom: 10px" type="success">
|
||||
@@ -83,6 +83,14 @@ export default {
|
||||
hidden: (formValue) => formValue['autofill'] !== 'script'
|
||||
}
|
||||
}
|
||||
},
|
||||
public: {
|
||||
disabled: this.protocol.name === 'winrm',
|
||||
hidden: (formValue) => {
|
||||
if (this.protocol.name === 'winrm') {
|
||||
formValue['public'] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,11 @@ export default {
|
||||
return params
|
||||
}
|
||||
const defaultTransformOption = (item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
if (typeof item === 'object') {
|
||||
return { label: item.name, value: item.id }
|
||||
} else {
|
||||
return { label: item, value: item }
|
||||
}
|
||||
}
|
||||
const transformOption = this.ajax.transformOption || defaultTransformOption
|
||||
const defaultFilterOption = (item) => {
|
||||
@@ -207,6 +211,12 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
disabled(newValue, oldValue) {
|
||||
this.selectDisabled = newValue
|
||||
},
|
||||
options(newValue, oldValue) {
|
||||
this.iOptions = newValue
|
||||
},
|
||||
iAjax(newValue, oldValue) {
|
||||
this.$log.debug('Select url changed: ', oldValue, ' => ', newValue)
|
||||
this.refresh()
|
||||
@@ -224,6 +234,7 @@ export default {
|
||||
this.$log.debug('Value is : ', this.value)
|
||||
this.iValue = this.value
|
||||
this.initialized = true
|
||||
this.$emit('initialized', true)
|
||||
}, 100)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
@@ -345,7 +356,7 @@ export default {
|
||||
})
|
||||
},
|
||||
clearSelected() {
|
||||
this.iValue = []
|
||||
this.iValue = this.multiple ? [] : ''
|
||||
},
|
||||
checkDisabled(item) {
|
||||
return item.disabled === undefined ? this.disabledValues.indexOf(item.value) !== -1 : item.disabled
|
||||
@@ -361,6 +372,7 @@ export default {
|
||||
this.refresh()
|
||||
this.$log.debug('Visible change, refresh select2')
|
||||
}
|
||||
this.$emit('visible-change', visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
:is="component"
|
||||
ref="SearchInput"
|
||||
v-model.trim="filterValue"
|
||||
:type="inputType"
|
||||
:fetch-suggestions="autocomplete"
|
||||
:placeholder="this.$t('common.EnterToContinue')"
|
||||
:type="inputType"
|
||||
class="search-input"
|
||||
@blur="focus = false"
|
||||
@change="handleConfirm"
|
||||
@focus="focus = true"
|
||||
@change="handleChange"
|
||||
@select="handleSelect"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
@@ -31,7 +31,7 @@
|
||||
class="show-password"
|
||||
@click="handleShowPassword"
|
||||
>
|
||||
<i class="fa" :class="[isCheckShowPassword ? 'fa-eye-slash' : 'fa-eye']" />
|
||||
<i :class="[isCheckShowPassword ? 'fa-eye-slash' : 'fa-eye']" class="fa" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -99,6 +99,9 @@ export default {
|
||||
this.filterValue = item.value
|
||||
this.handleConfirm()
|
||||
},
|
||||
handleChange: _.debounce(function(item) {
|
||||
this.handleConfirm()
|
||||
}, 240),
|
||||
handleConfirm() {
|
||||
if (this.filterValue === '') return
|
||||
|
||||
@@ -146,8 +149,7 @@ export default {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding-left: 2px;
|
||||
padding-bottom: 3px;
|
||||
padding: 1px 2px 1px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 1px;
|
||||
background-color: #fff;
|
||||
@@ -158,12 +160,12 @@ export default {
|
||||
}
|
||||
|
||||
&>>> .el-tag {
|
||||
margin-top: 3px;
|
||||
margin-top: 1px;
|
||||
font-family: sans-serif !important;
|
||||
}
|
||||
|
||||
&>>> .el-autocomplete {
|
||||
height: 26px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +174,7 @@ export default {
|
||||
&>>> .el-input__inner {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
padding-left: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +184,7 @@ export default {
|
||||
}
|
||||
|
||||
.filter-field >>> .el-input__inner {
|
||||
height: 26px;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.show-password {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="input-text">
|
||||
{{ value.toString() }}
|
||||
<div :class="bolder ? 'bolder' : ''" class="input-text">
|
||||
{{ value.toString() || text }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,15 @@ export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Boolean],
|
||||
default: () => false
|
||||
default: ''
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
bolder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -20,12 +28,14 @@ export default {
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.input-text {
|
||||
border: solid 1px #dcdfe6;
|
||||
line-height: 32px;
|
||||
padding-left: 5px;
|
||||
padding-left: 8px;
|
||||
height: 32px;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.bolder {
|
||||
border: solid 1px #dcdfe6;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
173
src/components/Form/FormFields/TransferSelect.vue
Normal file
173
src/components/Form/FormFields/TransferSelect.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<Select2
|
||||
ref="select2"
|
||||
v-model="iValue"
|
||||
v-bind="select2"
|
||||
@initialized="handleSelectInitialed"
|
||||
@input="onInputChange"
|
||||
v-on="$listeners"
|
||||
@focus.stop.prevent="handleFocus"
|
||||
/>
|
||||
<Dialog
|
||||
v-if="showTransfer"
|
||||
:loading-status="!isLoaded"
|
||||
:close-on-click-modal="false"
|
||||
:title="label"
|
||||
:visible.sync="showTransfer"
|
||||
class="the-dialog"
|
||||
width="730px"
|
||||
@cancel="handleTransCancel"
|
||||
@confirm="handleTransConfirm"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<krryPaging v-if="selectInitialized" ref="pageTransfer" class="transfer" v-bind="pagingTransfer" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import krryPaging from '@/components/Libs/Krry/paging/index.vue'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'TransferSelect',
|
||||
components: { krryPaging, Select2, Dialog },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
transformOption: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
ajax: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
const transformOption = vm.transformOption || vm.ajax.transformOption || ((item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
})
|
||||
const url = vm.url || vm.ajax.url
|
||||
const getPageData = async({ pageIndex, pageSize, keyword }) => {
|
||||
const limit = pageSize
|
||||
const offset = (pageIndex - 1) * pageSize
|
||||
const params = {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'fields_size': 'mini'
|
||||
}
|
||||
if (keyword) {
|
||||
params['search'] = keyword
|
||||
}
|
||||
this.isLoaded = false
|
||||
const data = await this.$axios.get(url, { params })
|
||||
this.isLoaded = true
|
||||
return data['results'].map(item => {
|
||||
const n = transformOption(item)
|
||||
return { id: n.value, label: n.label }
|
||||
})
|
||||
}
|
||||
return {
|
||||
isLoaded: false,
|
||||
showTransfer: false,
|
||||
selectInitialized: false,
|
||||
select2: {
|
||||
options: [],
|
||||
multiple: true,
|
||||
disabled: this.disabled,
|
||||
ajax: {
|
||||
url: url,
|
||||
transformOption: transformOption
|
||||
}
|
||||
},
|
||||
transferLoading: false,
|
||||
pagingTransfer: {
|
||||
pageSize: 100,
|
||||
filterable: true,
|
||||
async: true,
|
||||
dataList: [],
|
||||
getPageData: function(pageIndex, pageSize) {
|
||||
return getPageData({ pageIndex, pageSize })
|
||||
},
|
||||
getSearchData: async function(keyword, pageIndex, pageSize) {
|
||||
return getPageData({ keyword, pageIndex, pageSize })
|
||||
},
|
||||
selectedData: [],
|
||||
showClearBtn: true
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iValue: {
|
||||
get() {
|
||||
let value = this.value
|
||||
if (!value || value.length === 0) {
|
||||
return []
|
||||
}
|
||||
if (typeof value[0] === 'object') {
|
||||
value = value.map(item => {
|
||||
return item.id
|
||||
})
|
||||
}
|
||||
return _.uniq(value)
|
||||
},
|
||||
set(val) {
|
||||
this.emit(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emit(val) {
|
||||
const value = _.uniq(val)
|
||||
this.$emit('input', value)
|
||||
},
|
||||
onInputChange(val) {
|
||||
this.emit(val)
|
||||
},
|
||||
handleFocus() {
|
||||
this.$refs.select2.selectRef.blur()
|
||||
this.pagingTransfer.selectedData = this.$refs.select2.iOptions.map(item => {
|
||||
return { id: item.value, label: item.label }
|
||||
}).filter(item => {
|
||||
return this.iValue.includes(item.id)
|
||||
})
|
||||
this.showTransfer = true
|
||||
},
|
||||
handleSelectInitialed() {
|
||||
this.selectInitialized = true
|
||||
},
|
||||
handleTransCancel() {
|
||||
this.showTransfer = false
|
||||
},
|
||||
handleTransConfirm() {
|
||||
const selectedData = this.$refs.pageTransfer.selectListCheck
|
||||
const options = selectedData.map(item => {
|
||||
return { value: item.id, label: item.label }
|
||||
})
|
||||
this.select2.options = options
|
||||
this.emit(options.map(item => item.value))
|
||||
this.showTransfer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
<div v-if="tip !== ''" class="help-block">{{ tip }}</div>
|
||||
<input v-model="value" hidden type="text" v-on="$listeners">
|
||||
<div>
|
||||
<img :src="preview" v-bind="$attrs">
|
||||
<img :class="showBG ? 'show-bg' : ''" :src="preview" v-bind="$attrs">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,6 +27,10 @@ export default {
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
showBG: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -74,6 +78,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-bg {
|
||||
background-color: var(--banner-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
patterns.push([/\d/, i18n.t('common.password.NUMBER_REQUIRED')])
|
||||
}
|
||||
if (passwordRule['SECURITY_PASSWORD_SPECIAL_CHAR']) {
|
||||
const pattern = new RegExp("[`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“'。,、?]")
|
||||
const pattern = new RegExp("[`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“'。,、?_+-]")
|
||||
patterns.push([pattern, i18n.t('common.password.SPECIAL_CHAR_REQUIRED')])
|
||||
}
|
||||
for (const [pattern, msg] of patterns) {
|
||||
|
||||
@@ -204,7 +204,12 @@ export default {
|
||||
},
|
||||
formatWeektime(col) {
|
||||
const timeStamp = 1542384000000 // '2018-11-17 00:00:00'
|
||||
const beginStamp = timeStamp + col * 1800000 // col * 30 * 60 * 1000
|
||||
const timezone = 8
|
||||
const offsetGMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟
|
||||
const nowDate = new Date(timeStamp).getTime()
|
||||
const targetStamp = new Date(nowDate + offsetGMT * 60 * 1000 + timezone * 60 * 60 * 1000).getTime()
|
||||
|
||||
const beginStamp = targetStamp + col * 1800000 // col * 30 * 60 * 1000
|
||||
const endStamp = beginStamp + 1800000
|
||||
|
||||
const begin = this.formatDate(new Date(beginStamp), 'hh:mm')
|
||||
|
||||
6
src/components/Libs/Krry/cascader/index.js
Normal file
6
src/components/Libs/Krry/cascader/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import krryCascader from './index.vue'
|
||||
|
||||
// 组件的 name 作为组件调用名
|
||||
krryCascader.install = Vue => Vue.component(krryCascader.name, krryCascader)
|
||||
|
||||
export default krryCascader
|
||||
75
src/components/Libs/Krry/cascader/index.vue
Normal file
75
src/components/Libs/Krry/cascader/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="krry-main">
|
||||
<!-- 地区 -->
|
||||
<krry-container
|
||||
:box-operation="boxOperation"
|
||||
:box-title="boxTitle"
|
||||
:data-obj="dataObj"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
:filterable="filterable"
|
||||
:on-change-selected="emitChangeSelected"
|
||||
:selected-data="selectedData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import krryContainer from './models/container'
|
||||
|
||||
export default {
|
||||
name: 'KrCascader',
|
||||
components: {
|
||||
krryContainer
|
||||
},
|
||||
props: {
|
||||
boxTitle: {
|
||||
type: Array,
|
||||
default: () => ['省份', '城市', '区县', '选中地域']
|
||||
},
|
||||
boxOperation: {
|
||||
type: Array,
|
||||
default: () => ['添加省份', '添加城市', '添加区县', '删除地域']
|
||||
},
|
||||
dataObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
selectedData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: () => '请输入搜索内容'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasSelectData: []
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
created() {},
|
||||
methods: {
|
||||
// 获取已选数据的监听事件
|
||||
emitChangeSelected(val) {
|
||||
this.hasSelectData = val
|
||||
this.$emit('onChange', val)
|
||||
},
|
||||
// 提供获取已选数据的钩子
|
||||
getSelectedData() {
|
||||
return this.hasSelectData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.krry-main {
|
||||
min-width: 906px;
|
||||
}
|
||||
</style>
|
||||
370
src/components/Libs/Krry/cascader/models/container.vue
Normal file
370
src/components/Libs/Krry/cascader/models/container.vue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div>
|
||||
<krry-box
|
||||
ref="prov"
|
||||
:operation="boxOperation[0]"
|
||||
:title="boxTitle[0]"
|
||||
:operate-id="0"
|
||||
:district-list="provinceList"
|
||||
:filterable="filterable"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
@check-district="checkProvince"
|
||||
@selected-checked="selectedProvince"
|
||||
/>
|
||||
<krry-box
|
||||
ref="city"
|
||||
:operation="boxOperation[1]"
|
||||
:title="boxTitle[1]"
|
||||
:operate-id="1"
|
||||
:district-list="cityList"
|
||||
:filterable="filterable"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
@check-district="checkCity"
|
||||
@selected-checked="selectedCity"
|
||||
/>
|
||||
<krry-box
|
||||
ref="county"
|
||||
:operation="boxOperation[2]"
|
||||
:title="boxTitle[2]"
|
||||
:operate-id="2"
|
||||
:district-list="countyList"
|
||||
:filterable="filterable"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
@selected-checked="selectedCountry"
|
||||
/>
|
||||
<span class="inner-center el-icon-d-arrow-right" />
|
||||
<krry-box
|
||||
style="width: 260px"
|
||||
:operation="boxOperation[3]"
|
||||
:title="boxTitle[3]"
|
||||
:district-list="checkedDistrict"
|
||||
:filterable="filterable"
|
||||
:filter-placeholder="filterPlaceholder"
|
||||
@delete-checked="deleteCheck"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import krryBox from './models/box'
|
||||
export default {
|
||||
components: {
|
||||
krryBox
|
||||
},
|
||||
props: {
|
||||
boxTitle: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
boxOperation: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 地域数据
|
||||
dataObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
// 已选数据
|
||||
selectedData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
onChangeSelected: {
|
||||
type: Function,
|
||||
default: () => () => {}
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
flag: false, // 分仓对应的省id变量的监听器的锁,第一次触发不执行,数据还未初始化
|
||||
provinceList: [], // 省级数据
|
||||
cityList: [], // 市级数据
|
||||
countyList: [], // 区县级数据
|
||||
checkedDistrict: [], // 已选数据
|
||||
filterProvince: [], // 省级过滤id
|
||||
filterCity: [], // 市级过滤id
|
||||
filterCounty: [] // 区县级过滤id
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 映射出选中区域的数据Id
|
||||
selectDistrictId() {
|
||||
return this.checkedDistrict.map((val) => val.id)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dataObj: {
|
||||
handler() {
|
||||
this.getDistrict()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
selectedData: {
|
||||
handler() {
|
||||
this.getDistrict()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
checkedDistrict(newVal) {
|
||||
this.onChangeSelected(newVal)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getDistrict()
|
||||
},
|
||||
methods: {
|
||||
// 获取区域数据
|
||||
async getDistrict() {
|
||||
// 从后台传回经过处理的数据
|
||||
this.flag = true // 数据加载完成,解锁
|
||||
// 执行已选数据的过滤
|
||||
this.checkedDistrict = JSON.parse(JSON.stringify(this.selectedData))
|
||||
this.initFilter(this.checkedDistrict)
|
||||
// 获取省级数据
|
||||
this.getProvince()
|
||||
},
|
||||
// 获取省级数据
|
||||
getProvince() {
|
||||
this.provinceList = [] // 首先清空
|
||||
for (const key in this.dataObj.province) {
|
||||
this.provinceList.push({
|
||||
id: key,
|
||||
label: this.dataObj.province[key]
|
||||
})
|
||||
// 省级过滤处理
|
||||
this.handleFilterProvince()
|
||||
}
|
||||
},
|
||||
// 获取市级数据,子组件自定义的穿梭框传回的数据,val:[区域obj, 区域obj,...]
|
||||
checkProvince(val) {
|
||||
const obj = val[val.length - 1]
|
||||
let flag = true
|
||||
if (obj !== undefined) {
|
||||
const id = obj.id
|
||||
for (const key in this.dataObj.city) {
|
||||
if (id === key) {
|
||||
// 匹配到的id,将对应的市级数据传递到子组件
|
||||
this.cityList = this.dataObj.city[key]
|
||||
// 过滤处理
|
||||
this.handleFilterCity()
|
||||
// 过滤处理
|
||||
// 再清空上一次的县级数据
|
||||
this.countyList = []
|
||||
// 将父级对象放进市级组件
|
||||
this.$refs.city.father = {
|
||||
id: id,
|
||||
label: obj.label
|
||||
}
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果市级没有匹配到,市级和区级都显示为空
|
||||
if (flag) {
|
||||
this.cityList = []
|
||||
this.countyList = []
|
||||
}
|
||||
},
|
||||
// 获取县级数据,子组件自定义的穿梭框传回的数据,val:[区域obj, 区域obj,...]
|
||||
checkCity(val) {
|
||||
const obj = val[val.length - 1]
|
||||
let flag = true
|
||||
if (obj !== undefined) {
|
||||
const id = obj.id
|
||||
for (const key in this.dataObj.county) {
|
||||
if (id.toString() === key) {
|
||||
// 匹配到的id,将对应的区级数据传递到子组件
|
||||
this.countyList = this.dataObj.county[key]
|
||||
// 过滤处理
|
||||
this.handleFilterCounty()
|
||||
// 获取省级的数据
|
||||
const fatherId = this.$refs.city.father.id
|
||||
const fatherText = this.$refs.city.father.label
|
||||
// 拼接上市级数据放进县级组件
|
||||
this.$refs.county.father = {
|
||||
id: fatherId + '-' + id,
|
||||
label: fatherText + '-' + obj.label
|
||||
}
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 区级没有匹配到,显示为空
|
||||
if (flag) {
|
||||
this.countyList = []
|
||||
}
|
||||
},
|
||||
// 从省级添加到已选区域,参数:val:省级对象数组,filterId:所选择的省级id数组
|
||||
selectedProvince(val, filterId) {
|
||||
this.checkedDistrict = this.checkedDistrict.concat(val)
|
||||
this.filterProvince = this.filterProvince.concat(filterId)
|
||||
// 如果过滤的市级区域,还有县级区域,合并成一个市级
|
||||
for (const val of filterId) {
|
||||
for (const vq of this.checkedDistrict) {
|
||||
const selectId = vq.id.split('-')
|
||||
// 拆分的数组长度大于1,说明有市级以下的区域,合并成一个省级区域
|
||||
if (selectId.length > 1 && selectId[0] === val) {
|
||||
// 在已选择的区域中删除市级数据,合并成一个省级
|
||||
this.checkedDistrict = this.checkedDistrict.filter(
|
||||
(vl) => vl !== vq
|
||||
)
|
||||
// 当前省级已被合并,从过滤数组中删除该市级和县级数据
|
||||
this.filterCity = this.filterCity.filter(
|
||||
(vf) => vf.toString() !== selectId[1]
|
||||
)
|
||||
this.filterCounty = this.filterCounty.filter(
|
||||
(vs) => vs.toString() !== selectId[2]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 清空下面的市级和县级区域
|
||||
this.cityList = []
|
||||
this.countyList = []
|
||||
// 过滤处理
|
||||
this.handleFilterProvince()
|
||||
},
|
||||
// 从市级添加到已选区域
|
||||
selectedCity(val, filterId) {
|
||||
this.checkedDistrict = this.checkedDistrict.concat(val)
|
||||
this.filterCity = this.filterCity.concat(filterId)
|
||||
// 如果过滤的市级区域,还有县级区域,合并成一个市级
|
||||
for (const val of filterId) {
|
||||
for (const vq of this.checkedDistrict) {
|
||||
const selectId = vq.id.split('-')
|
||||
// 拆分的数组长度为3,说明有县级区域,并且该市级区域与当前加入市级区域的id相同,合并成一个市级区域
|
||||
if (selectId.length === 3 && selectId[1] === val.toString()) {
|
||||
// 在已选择的区域中删除县级数据,合并成一个市级
|
||||
this.checkedDistrict = this.checkedDistrict.filter(
|
||||
(vl) => vl !== vq
|
||||
)
|
||||
// 当前市级已被合并,从过滤数组中删除该县级数据
|
||||
this.filterCounty = this.filterCounty.filter(
|
||||
(vs) => vs.toString() !== selectId[2]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 清空下面的县级区域
|
||||
this.countyList = []
|
||||
// 过滤处理
|
||||
this.handleFilterCity()
|
||||
},
|
||||
// 从县级添加到已选区域
|
||||
selectedCountry(val, filterId) {
|
||||
this.checkedDistrict = this.checkedDistrict.concat(val)
|
||||
this.filterCounty = this.filterCounty.concat(filterId)
|
||||
// 过滤处理
|
||||
this.handleFilterCounty()
|
||||
},
|
||||
// 省级过滤处理
|
||||
handleFilterProvince() {
|
||||
let newPro = Array.from(this.provinceList)
|
||||
for (const val of this.filterProvince) {
|
||||
newPro = newPro.filter((vq) => String(vq.id) !== String(val))
|
||||
}
|
||||
this.provinceList = Array.from(newPro)
|
||||
},
|
||||
// 市级过滤处理
|
||||
handleFilterCity() {
|
||||
let newCity = Array.from(this.cityList)
|
||||
for (const val of this.filterCity) {
|
||||
newCity = newCity.filter((vq) => String(vq.id) !== String(val))
|
||||
}
|
||||
this.cityList = Array.from(newCity)
|
||||
},
|
||||
// 区县级过滤处理
|
||||
handleFilterCounty() {
|
||||
let newCounty = Array.from(this.countyList)
|
||||
for (const val of this.filterCounty) {
|
||||
newCounty = newCounty.filter((vq) => String(vq.id) !== String(val))
|
||||
}
|
||||
this.countyList = Array.from(newCounty)
|
||||
},
|
||||
// 删除已选区域,参数:deleteVal:要删除的区域对象数组
|
||||
deleteCheck(deleteVal) {
|
||||
for (const val of deleteVal) {
|
||||
const selectId = val.id.split('-')
|
||||
const length = selectId.length
|
||||
switch (length) {
|
||||
case 1: {
|
||||
// 长度只有1,只有省级数据,删除对应省级的filter中的数据
|
||||
this.filterProvince = this.filterProvince.filter(
|
||||
(vs) => vs !== selectId[0]
|
||||
)
|
||||
// 重新获取县级数据
|
||||
this.getProvince()
|
||||
break
|
||||
}
|
||||
case 2: {
|
||||
// 长度为2,到达市级数据,删除对应市级的filter中的数据
|
||||
this.filterCity = this.filterCity.filter(
|
||||
(vs) => vs.toString() !== selectId[1]
|
||||
)
|
||||
// 重新获取市级数据
|
||||
if (this.$refs.prov.selectedDistrict.length) {
|
||||
// 省级已勾选才显示区级
|
||||
this.checkProvince([this.$refs.city.father])
|
||||
}
|
||||
break
|
||||
}
|
||||
case 3: {
|
||||
// 长度为3,到达县级数据,删除对应县级的filter中的数据
|
||||
this.filterCounty = this.filterCounty.filter(
|
||||
(vs) => vs.toString() !== selectId[2]
|
||||
)
|
||||
if (this.$refs.city.selectedDistrict.length) {
|
||||
// 市级已勾选才显示区级
|
||||
const fatherId = this.$refs.county.father.id.split('-')[1]
|
||||
const fatherText = this.$refs.county.father.label.split('-')[1]
|
||||
const obj = [{ id: fatherId, label: fatherText }]
|
||||
// 重新获取县级数据,参数:当前市级ID的对象数组:obj:[{id:id,label:label}]
|
||||
this.checkCity(obj)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 刷新已选区域
|
||||
this.checkedDistrict = this.checkedDistrict.filter(
|
||||
(vd) => vd.id !== val.id
|
||||
)
|
||||
}
|
||||
},
|
||||
// 初始化过滤器 参数:addVal:要增加的区域对象数组
|
||||
initFilter(addVal) {
|
||||
for (const val of addVal) {
|
||||
const selectId = val.id.split('-')
|
||||
const length = selectId.length
|
||||
switch (length) {
|
||||
case 1:
|
||||
this.filterProvince.push(selectId[0])
|
||||
break
|
||||
case 2:
|
||||
this.filterCity.push(selectId[1])
|
||||
break
|
||||
case 3:
|
||||
this.filterCounty.push(selectId[2])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.inner-center {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
248
src/components/Libs/Krry/cascader/models/models/box.vue
Normal file
248
src/components/Libs/Krry/cascader/models/models/box.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="el-transfer-panel district-panel">
|
||||
<div class="el-transfer-panel__header">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>{{ title }}</el-checkbox>
|
||||
<span
|
||||
class="check-number"
|
||||
>{{ selectedDistrict.length }}/{{ districtListMock.length }}</span>
|
||||
</div>
|
||||
<div class="el-transfer-panel__body">
|
||||
<div
|
||||
v-if="filterable"
|
||||
class="el-transfer-panel__filter el-input el-input--small el-input--prefix"
|
||||
>
|
||||
<input
|
||||
v-model="searchWord"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="filterPlaceholder"
|
||||
class="el-input__inner"
|
||||
>
|
||||
<span class="el-input__prefix" style="left: 0px">
|
||||
<i class="el-input__icon el-icon-search" />
|
||||
</span>
|
||||
</div>
|
||||
<el-checkbox-group
|
||||
v-if="districtListMock.length > 0"
|
||||
v-model="selectedDistrict"
|
||||
:class="{ expand: !filterable }"
|
||||
@change="handleCheckedChange"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(city, index) in districtListMock"
|
||||
:key="index"
|
||||
class="el-transfer-panel__item"
|
||||
:disabled="city.disabled"
|
||||
:title="city.label"
|
||||
:label="city"
|
||||
>{{ city.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<p v-else class="no-data">无数据</p>
|
||||
</div>
|
||||
<div class="vip-footer">
|
||||
<el-button
|
||||
type="text"
|
||||
:disabled="selectedDistrict.length<=0"
|
||||
size="small"
|
||||
round
|
||||
@click="checkedSelected"
|
||||
>
|
||||
<span>{{ operation }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
operateId: {
|
||||
type: Number,
|
||||
default: () => 0
|
||||
},
|
||||
// 区域数据
|
||||
districtList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
districtListMock: [], // 展示的数据 (搜索会自动修改这个数组)
|
||||
selectedDistrict: [], // 已选择,数据格式:[区域id,id,id...]
|
||||
father: {}, // 父级数据
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
searchWord: '',
|
||||
buttonAble: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 搜索框的监听器
|
||||
searchWord(newWord, oldWord) {
|
||||
// 重新获取数据
|
||||
this.districtListMock = this.districtList
|
||||
// 过滤掉数据,保留搜索的数据
|
||||
this.districtListMock = this.districtListMock.filter((val) =>
|
||||
val.label.includes(newWord)
|
||||
)
|
||||
},
|
||||
// 当点击省级或市级,自动监听并更新市级或区级的列表
|
||||
districtList() {
|
||||
this.getDistrict()
|
||||
// 如果区域数据为空,则已选择的数据也要清空
|
||||
if (this.districtList.length === 0) {
|
||||
this.selectedDistrict = []
|
||||
}
|
||||
},
|
||||
// districtListMock 和 checkAll 的监听器
|
||||
districtListMock() {
|
||||
// 当方框中无已选择的数据时,不能勾选checkBox
|
||||
if (this.selectedDistrict.length === 0) {
|
||||
this.checkAll = false
|
||||
this.isIndeterminate = false
|
||||
}
|
||||
},
|
||||
// 当列表中无数据时,不能勾选checkBox
|
||||
checkAll() {
|
||||
this.checkAll = this.districtListMock.length === 0 ? false : this.checkAll
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getDistrict()
|
||||
},
|
||||
methods: {
|
||||
// 获取区域数据
|
||||
getDistrict() {
|
||||
this.districtListMock = this.districtList
|
||||
// 已选择的清空
|
||||
this.selectedDistrict = []
|
||||
},
|
||||
// 单选
|
||||
handleCheckedChange(value) {
|
||||
const checkedCount = value.length
|
||||
this.checkAll = checkedCount === this.districtListMock.length
|
||||
this.isIndeterminate =
|
||||
checkedCount > 0 && checkedCount < this.districtListMock.length
|
||||
this.$emit('check-district', value)
|
||||
},
|
||||
// 全选
|
||||
handleCheckAllChange(val) {
|
||||
this.selectedDistrict = val ? this.districtListMock.filter(val => !val.disabled).map((val) => val) : []
|
||||
this.isIndeterminate = false
|
||||
},
|
||||
// 添加至已选 或 删除已选区域
|
||||
checkedSelected() {
|
||||
const selectedList = []
|
||||
const filterId = []
|
||||
if (this.operateId === 0) {
|
||||
// 省级添加
|
||||
for (const val of this.selectedDistrict) {
|
||||
selectedList.push({
|
||||
id: val.id,
|
||||
label: val.label
|
||||
})
|
||||
filterId.push(val.id)
|
||||
}
|
||||
this.$emit('selected-checked', selectedList, filterId)
|
||||
} else if (this.operateId === 1 || this.operateId === 2) {
|
||||
// 市级或县级添加
|
||||
for (const val of this.selectedDistrict) {
|
||||
selectedList.push({
|
||||
id: this.father.id + '-' + val.id,
|
||||
label: this.father.label + '-' + val.label
|
||||
})
|
||||
filterId.push(val.id)
|
||||
}
|
||||
this.$emit('selected-checked', selectedList, filterId)
|
||||
} else {
|
||||
// 删除已选区域
|
||||
for (const val of this.selectedDistrict) {
|
||||
selectedList.push({
|
||||
id: val.id,
|
||||
label: val.label
|
||||
})
|
||||
}
|
||||
this.$emit('delete-checked', selectedList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.district-panel {
|
||||
width: 200px;
|
||||
|
||||
.el-transfer-panel__header {
|
||||
.el-checkbox {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.el-transfer-panel__body {
|
||||
height: 292px;
|
||||
padding: 6px 0;
|
||||
|
||||
.el-transfer-panel__filter {
|
||||
line-height: 0;
|
||||
margin: 6px 14px 12px;
|
||||
}
|
||||
}
|
||||
.el-checkbox-group {
|
||||
height: 240px;
|
||||
overflow: auto;
|
||||
&.expand {
|
||||
height: 290px;
|
||||
}
|
||||
|
||||
.el-transfer-panel__item {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.check-number {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 0;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.no-data {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 6px 15px 0;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
.vip-footer {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
443
src/components/Libs/Krry/paging/index.vue
Normal file
443
src/components/Libs/Krry/paging/index.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div class="krry-main">
|
||||
<el-row :gutter="10">
|
||||
<el-col :md="10" :sm="24">
|
||||
<krry-box
|
||||
ref="noSelect"
|
||||
:async="async"
|
||||
:async-search-flag="asyncSearchFlag"
|
||||
:data-show-list="notSelectDataList"
|
||||
:filter-placeholder="filterPlaceholder[0] || $tc('common.Search')"
|
||||
:filterable="filterable"
|
||||
:highlight-color="highlightColor"
|
||||
:is-highlight="isHighlight"
|
||||
:is-last-page="isLastPage"
|
||||
:operate-id="0"
|
||||
:page-size="pageSize"
|
||||
:page-texts="pageTexts"
|
||||
:show-clear-btn="showClearBtn"
|
||||
:title="boxTitle[0] || $tc('common.Selection')"
|
||||
@check-district="noCheckSelect"
|
||||
@search-word="searchWord"
|
||||
@check-disable="checkDisable"
|
||||
@get-data="getData"
|
||||
@get-data-by-keyword="getDataByKeyword"
|
||||
@clear-input="clearQueryInp('left')"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :md="4" :sm="24" class="buttons">
|
||||
<div class="opera">
|
||||
<el-button
|
||||
:disabled="disablePre"
|
||||
class="el-transfer__button"
|
||||
icon="el-icon-arrow-left"
|
||||
size="mini"
|
||||
@click="deleteData"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="disableNex"
|
||||
class="el-transfer__button"
|
||||
icon="el-icon-arrow-right"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="addData"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="10" :sm="24">
|
||||
<krry-box
|
||||
ref="hasSelect"
|
||||
:data-show-list="checkedData"
|
||||
:filter-placeholder="filterPlaceholder[1] || $tc('common.Search')"
|
||||
:filterable="filterable"
|
||||
:highlight-color="highlightColor"
|
||||
:is-highlight="isHighlight"
|
||||
:operate-id="1"
|
||||
:page-size="pageSize"
|
||||
:page-texts="pageTexts"
|
||||
:show-clear-btn="showClearBtn"
|
||||
:title="boxTitle[1] || $tc('common.Selected')"
|
||||
@check-district="hasCheckSelect"
|
||||
@search-word="searchWord"
|
||||
@check-disable="checkDisable"
|
||||
@clear-input="clearQueryInp('right')"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import krryBox from './models/box'
|
||||
|
||||
export default {
|
||||
name: 'KrryPaging',
|
||||
components: {
|
||||
krryBox
|
||||
},
|
||||
props: {
|
||||
boxTitle: {
|
||||
type: Array,
|
||||
// default: () => [this.$tc('common.Selection'), this.$tc('common.Selected')]
|
||||
default: () => ['', '']
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 160
|
||||
},
|
||||
dataList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: Array,
|
||||
default: () => ['', '']
|
||||
// default: () => [this.$tc('common.Search'), this.$tc('common.Search')]
|
||||
},
|
||||
pageTexts: {
|
||||
type: Array,
|
||||
default: () => ['', '']
|
||||
// default: () => ['< ' + this.$tc('common.PagePrev'), this.$tc('common.PageNext') + ' >']
|
||||
},
|
||||
sort: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
async: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
getPageData: {
|
||||
type: Function,
|
||||
default: () => []
|
||||
},
|
||||
getSearchData: {
|
||||
type: Function,
|
||||
default: () => []
|
||||
},
|
||||
isHighlight: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
highlightColor: {
|
||||
type: String,
|
||||
default: () => '#ff2b2b'
|
||||
},
|
||||
showClearBtn: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notSelectDataList: [], // 未选中(已过滤出已选)的数据
|
||||
checkedData: [], // 已选中的数据
|
||||
|
||||
dataListNoCheck: [], // 未搜索的数据
|
||||
selectListCheck: [], // 未搜索的数据
|
||||
|
||||
noCheckData: [], // 未选中区域的已勾选的数据(待添加到已选区域)
|
||||
hasCheckData: [], // 已选中区域的已勾选的数据(从未选区域中待删除)
|
||||
|
||||
noSelectKeyword: '',
|
||||
haSelectKeyword: '',
|
||||
|
||||
disablePre: true,
|
||||
disableNex: true,
|
||||
|
||||
manualEmpty: false, // 是否手动将已选区数据置为空
|
||||
|
||||
asyncDataList: [], // 异步请求的数据源
|
||||
isLastPage: false // 异步请求是否是最后一页
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 传递到后台保存的数据(已选中的数据的 id 数组)
|
||||
selectIdList() {
|
||||
return this.selectListCheck.map(item => item.id)
|
||||
},
|
||||
originList() {
|
||||
return this.async ? this.asyncDataList : this.dataList
|
||||
},
|
||||
asyncSearchFlag() {
|
||||
// 是否设置了异步搜索方法
|
||||
return this.async && this.getSearchData !== undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectIdList(newVal) {
|
||||
// 获取已选数据的监听事件
|
||||
const moveKeys = [
|
||||
...this.noCheckData.map(item => item.id),
|
||||
...this.hasCheckData.map(item => item.id)
|
||||
]
|
||||
this.hasCheckData = []
|
||||
this.noCheckData = []
|
||||
this.$emit('onChange', newVal, moveKeys)
|
||||
},
|
||||
dataList: {
|
||||
handler() {
|
||||
!this.async && this.initData()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
selectedData: {
|
||||
handler() {
|
||||
this.initData(true)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.async ? this.getData(1, true) : this.initData(true)
|
||||
},
|
||||
methods: {
|
||||
// 分页数据,初始化数据,过滤已选数据
|
||||
initData(selectedChange) {
|
||||
// this.checkedData 为空 且 从来没有将已选区置为空,则从 selectedData 获取
|
||||
if ((!this.checkedData.length && !this.manualEmpty) || selectedChange) {
|
||||
this.checkedData = JSON.parse(JSON.stringify(this.selectedData))
|
||||
const keywords = this.$refs.hasSelect
|
||||
? this.$refs.hasSelect.searchWord
|
||||
: ''
|
||||
keywords && this.searchWord(keywords, 1)
|
||||
}
|
||||
if (!this.async) {
|
||||
this.selectListCheck = JSON.parse(JSON.stringify(this.checkedData))
|
||||
const checkDataId = this.selectListCheck.map(ele => ele.id)
|
||||
this.notSelectDataList = this.originList.filter(
|
||||
ele => !checkDataId.includes(ele.id)
|
||||
)
|
||||
this.dataListNoCheck = JSON.parse(
|
||||
JSON.stringify(this.notSelectDataList)
|
||||
)
|
||||
} else {
|
||||
if (selectedChange) {
|
||||
this.selectListCheck = JSON.parse(JSON.stringify(this.checkedData))
|
||||
}
|
||||
const checkDataId = this.selectListCheck.map(ele => ele.id)
|
||||
this.notSelectDataList = this.originList.filter(
|
||||
ele =>
|
||||
!checkDataId.includes(ele.id) &&
|
||||
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
|
||||
)
|
||||
this.dataListNoCheck = this.originList.filter(
|
||||
ele => !checkDataId.includes(ele.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
searchWord(keyword, titleId) {
|
||||
// 过滤掉数据,保留搜索的数据
|
||||
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
|
||||
if (titleId === 0) {
|
||||
this.noSelectKeyword = keyword
|
||||
if (!this.asyncSearchFlag) {
|
||||
this.notSelectDataList = this.dataListNoCheck.filter(val =>
|
||||
val.label.includes(keyword)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.haSelectKeyword = keyword
|
||||
this.checkedData = this.selectListCheck.filter(val =>
|
||||
val.label.includes(keyword)
|
||||
)
|
||||
}
|
||||
const refsName = titleId === 0 ? 'noSelect' : 'hasSelect'
|
||||
// 延迟执行
|
||||
setTimeout(() => {
|
||||
!this.async && this.$refs[refsName].initData()
|
||||
}, 0)
|
||||
},
|
||||
// 检查左右按钮可用性
|
||||
checkDisable(data, operateId) {
|
||||
if (operateId === 0) {
|
||||
this.disableNex = !(data.length > 0)
|
||||
} else {
|
||||
this.disablePre = !(data.length > 0)
|
||||
}
|
||||
},
|
||||
// 未选中区域的选泽
|
||||
noCheckSelect(val) {
|
||||
this.noCheckData = val
|
||||
},
|
||||
// 已选中区域的选泽
|
||||
hasCheckSelect(val) {
|
||||
this.hasCheckData = val
|
||||
},
|
||||
// 关键:把未选择的数据当做已选择的过滤数组,把已选择的数据当做未选择的过滤数组,在全局data进行过滤,最后进行一次搜索
|
||||
// 添加至已选
|
||||
addData() {
|
||||
const noCheckDataId = this.noCheckData.map(ele => ele.id)
|
||||
// 待选区数据过滤
|
||||
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
|
||||
this.notSelectDataList = this.notSelectDataList.filter(
|
||||
ele =>
|
||||
!noCheckDataId.includes(ele.id) &&
|
||||
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
|
||||
)
|
||||
this.dataListNoCheck = this.dataListNoCheck.filter(
|
||||
ele => !noCheckDataId.includes(ele.id)
|
||||
)
|
||||
// 已选区数据增加
|
||||
if (!this.async && this.sort) {
|
||||
// 排序,从固定不变的所有数据中过滤,顺序就不会乱。但若数据量大就会比较卡
|
||||
// 异步分页不支持排序
|
||||
const dataListNoCheckId = this.dataListNoCheck.map(ele => ele.id)
|
||||
this.checkedData = this.originList.filter(
|
||||
ele =>
|
||||
!dataListNoCheckId.includes(ele.id) &&
|
||||
ele.label.includes(this.haSelectKeyword)
|
||||
)
|
||||
this.selectListCheck = this.originList.filter(
|
||||
ele => !dataListNoCheckId.includes(ele.id)
|
||||
)
|
||||
} else {
|
||||
// 这种效率更高的方法,但不能排序
|
||||
this.checkedData.push(...this.noCheckData)
|
||||
this.selectListCheck.push(...this.noCheckData)
|
||||
this.checkedData = this.checkedData.filter(ele =>
|
||||
ele.label.includes(this.haSelectKeyword)
|
||||
)
|
||||
}
|
||||
},
|
||||
// 从已选中删除
|
||||
deleteData() {
|
||||
// 已选区数据过滤
|
||||
const hasCheckDataId = this.hasCheckData.map(ele => ele.id)
|
||||
this.checkedData = this.checkedData.filter(
|
||||
ele =>
|
||||
!hasCheckDataId.includes(ele.id) &&
|
||||
ele.label.includes(this.haSelectKeyword)
|
||||
)
|
||||
this.selectListCheck = this.selectListCheck.filter(
|
||||
ele => !hasCheckDataId.includes(ele.id)
|
||||
)
|
||||
|
||||
this.manualEmpty = !this.checkedData.length
|
||||
|
||||
// 待选区数据增加
|
||||
const selectListCheckId = this.selectListCheck.map(ele => ele.id)
|
||||
// const checkedDataId = this.checkedData.map(ele => ele.id)
|
||||
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
|
||||
this.notSelectDataList = this.originList.filter(
|
||||
ele =>
|
||||
!selectListCheckId.includes(ele.id) &&
|
||||
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
|
||||
)
|
||||
this.dataListNoCheck = this.originList.filter(
|
||||
ele => !selectListCheckId.includes(ele.id)
|
||||
)
|
||||
},
|
||||
// 提供获取已选数据的钩子
|
||||
getSelectedData() {
|
||||
return this.selectIdList
|
||||
},
|
||||
clearQueryInp(position) {
|
||||
switch (position) {
|
||||
case 'left':
|
||||
this.$refs.noSelect.searchWord = ''
|
||||
this.asyncSearchFlag && this.getDataByKeyword('')
|
||||
break
|
||||
case 'right':
|
||||
this.$refs.hasSelect.searchWord = ''
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
async getDataByKeyword(keyword, pageIndex) {
|
||||
keyword = keyword.trim()
|
||||
if (keyword) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.noSelect.asyncSearch = true
|
||||
})
|
||||
const resData = await this.getSearchData(
|
||||
keyword,
|
||||
pageIndex,
|
||||
this.pageSize
|
||||
)
|
||||
if (Array.isArray(resData) && resData.length) {
|
||||
this.asyncDataList = resData
|
||||
this.notSelectDataList = resData
|
||||
this.initData()
|
||||
this.isLastPage = resData.length < this.pageSize
|
||||
} else {
|
||||
this.notSelectDataList = []
|
||||
this.isLastPage = true
|
||||
}
|
||||
} else {
|
||||
this.$refs.noSelect.asyncSearch = false
|
||||
await this.getData(1)
|
||||
}
|
||||
},
|
||||
async getData(pageIndex, changed = false) {
|
||||
this.$nextTick(() => {
|
||||
// 设置异步分页的 pageIndex
|
||||
this.$refs.noSelect.asyncPageIndex = pageIndex
|
||||
// 清空左侧输入框
|
||||
this.$refs.noSelect.searchWord = ''
|
||||
// asyncSearch 设置为 true
|
||||
this.$refs.noSelect.asyncSearch = false
|
||||
})
|
||||
const resData = await this.getPageData(pageIndex, this.pageSize)
|
||||
if (Array.isArray(resData) && resData.length) {
|
||||
this.asyncDataList = resData
|
||||
this.notSelectDataList = resData
|
||||
// 这里必须是 true,否则右侧不能搜索, 一搜索确认就不行了
|
||||
this.initData(changed)
|
||||
this.isLastPage = resData.length < this.pageSize
|
||||
} else {
|
||||
this.notSelectDataList = []
|
||||
this.isLastPage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.krry-main {
|
||||
min-width: 600px;
|
||||
}
|
||||
.inner-center {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.opera {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
margin: 180px 8px;
|
||||
width: 100%;
|
||||
@media screen and (max-width: 992px) {
|
||||
margin: 8px 8px;
|
||||
text-align:start
|
||||
}
|
||||
|
||||
.el-button.is-circle {
|
||||
border-radius: 50%;
|
||||
padding: 12px;
|
||||
display: block;
|
||||
margin: 25px auto;
|
||||
}
|
||||
.el-transfer__button {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
.el-transfer-panel__filter .el-input__inner {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
397
src/components/Libs/Krry/paging/models/box.vue
Normal file
397
src/components/Libs/Krry/paging/models/box.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<template>
|
||||
<div class="el-transfer-panel district-panel">
|
||||
<div class="el-transfer-panel__header">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
{{ title }}
|
||||
</el-checkbox>
|
||||
<span class="check-number">
|
||||
{{ checkedData.length }}/{{ districtListMock.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="el-transfer-panel__body">
|
||||
<div
|
||||
v-if="filterable"
|
||||
class="el-transfer-panel__filter el-input el-input--mini el-input--prefix"
|
||||
>
|
||||
<input
|
||||
v-model.trim="searchWord"
|
||||
:class="{ showClear: showClearBtn }"
|
||||
:placeholder="filterPlaceholder"
|
||||
autocomplete="off"
|
||||
class="el-input__inner"
|
||||
type="text"
|
||||
@change="handleKeyword"
|
||||
>
|
||||
<span class="el-input__prefix" style="left: 0px">
|
||||
<i class="el-input__icon el-icon-search" />
|
||||
</span>
|
||||
<span v-if="searchWord && showClearBtn" class="clear-input">
|
||||
<i class="el-icon-circle-close" @click="clearInp" />
|
||||
</span>
|
||||
</div>
|
||||
<el-checkbox-group
|
||||
v-if="districtListMock.length > 0"
|
||||
v-model="checkedData"
|
||||
:class="{ expand: !filterable }"
|
||||
@change="handleCheckedChange"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(item, index) in districtListMock"
|
||||
:key="index"
|
||||
:disabled="item.disabled"
|
||||
:label="item"
|
||||
:title="item.label"
|
||||
class="el-transfer-panel__item"
|
||||
>
|
||||
<span v-html="isHighlight ? filterHighlight(item.label) : item.label" />
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<p v-else class="no-data">{{ this.$t('common.NoData') }}</p>
|
||||
</div>
|
||||
<div class="vip-footer">
|
||||
<el-button :disabled="disabledPre" class="v-page" plain small @click="prev">
|
||||
{{ pageTexts[0] || defaultPrev }}
|
||||
</el-button>
|
||||
<el-button :disabled="disabledNex" class="v-page" plain small @click="next">
|
||||
{{ pageTexts[1] || defaultNext }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
operateId: {
|
||||
type: Number,
|
||||
default: () => 0
|
||||
},
|
||||
dataShowList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: () => 10
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean
|
||||
},
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: () => 'Search'
|
||||
},
|
||||
pageTexts: {
|
||||
type: Array,
|
||||
default: () => ['', '']
|
||||
},
|
||||
async: {
|
||||
type: Boolean,
|
||||
default: () => false // 已选区不做异步
|
||||
},
|
||||
isLastPage: {
|
||||
type: Boolean
|
||||
},
|
||||
isHighlight: {
|
||||
type: Boolean
|
||||
},
|
||||
highlightColor: {
|
||||
type: String,
|
||||
default: () => '#409EFF'
|
||||
},
|
||||
asyncSearchFlag: {
|
||||
// 是否设置了异步搜索方法
|
||||
type: Boolean
|
||||
},
|
||||
showClearBtn: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
districtListMock: [], // 展示的数据 (搜索和分页会自动修改这个数组)
|
||||
checkedData: [], // 已选择,数据格式:[id,id,id...]
|
||||
isIndeterminate: false,
|
||||
checkAll: false,
|
||||
searchWord: '',
|
||||
len: 0,
|
||||
total: 0,
|
||||
pageIndex: 0,
|
||||
disabledPre: true,
|
||||
disabledNex: false,
|
||||
asyncSearch: false, // 要执行异步搜索的标记
|
||||
asyncPageIndex: 1, // 异步分页的 pageIndex
|
||||
asyncSearchPageIndex: 1, // 异步搜索的 pageIndex,
|
||||
defaultPrev: '< ' + this.$tc('common.PagePrev'),
|
||||
defaultNext: this.$tc('common.PageNext') + ' >'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 搜索框的监听器
|
||||
searchWord(newWord) {
|
||||
this.$emit('search-word', newWord, this.operateId)
|
||||
},
|
||||
// districtListMock 和 checkAll 的监听器
|
||||
districtListMock() {
|
||||
// 当方框中无已选择的数据时,不能勾选checkBox
|
||||
if (this.checkedData.length === 0) {
|
||||
this.checkAll = false
|
||||
this.isIndeterminate = false
|
||||
}
|
||||
},
|
||||
checkedData(newWord) {
|
||||
this.$emit('check-disable', newWord, this.operateId)
|
||||
},
|
||||
// 当列表中无数据时,不能勾选checkBox
|
||||
checkAll() {
|
||||
this.checkAll = this.districtListMock.length === 0 ? false : this.checkAll
|
||||
},
|
||||
dataShowList: {
|
||||
handler() {
|
||||
this.async ? this.asyncInitData() : this.initData()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initData()
|
||||
},
|
||||
methods: {
|
||||
handleKeyword() {
|
||||
this.asyncSearchPageIndex = 1
|
||||
this.asyncSearchFlag &&
|
||||
this.$emit(
|
||||
'get-data-by-keyword',
|
||||
this.searchWord,
|
||||
this.asyncSearchPageIndex
|
||||
)
|
||||
},
|
||||
// 分页数据
|
||||
initData() {
|
||||
this.len = this.dataShowList.length
|
||||
this.total = Math.ceil(this.len / this.pageSize)
|
||||
this.pageIndex = 0
|
||||
this.pageData()
|
||||
},
|
||||
pageData() {
|
||||
this.checkedData = []
|
||||
if (this.total > 1 && this.pageIndex < this.total - 1) {
|
||||
this.pageIndex === 0
|
||||
? (this.disabledPre = true)
|
||||
: (this.disabledPre = false)
|
||||
this.disabledNex = false
|
||||
this.districtListMock = this.dataShowList.slice(
|
||||
this.pageIndex * this.pageSize,
|
||||
this.pageIndex * this.pageSize + this.pageSize
|
||||
)
|
||||
} else {
|
||||
this.total > 1 ? (this.disabledPre = false) : (this.disabledPre = true)
|
||||
this.disabledNex = true
|
||||
this.districtListMock = this.dataShowList.slice(
|
||||
this.pageIndex * this.pageSize,
|
||||
this.len
|
||||
)
|
||||
}
|
||||
},
|
||||
// 异步获取的数据,检查分页按钮可用性
|
||||
asyncInitData() {
|
||||
// 取消勾选
|
||||
this.checkedData = []
|
||||
// 分页按钮可用性
|
||||
this.disabledNex = this.isLastPage
|
||||
this.disabledPre =
|
||||
this.asyncSearchFlag && this.asyncSearch
|
||||
? this.asyncSearchPageIndex <= 1
|
||||
: this.asyncPageIndex <= 1
|
||||
// 赋值
|
||||
this.districtListMock = this.dataShowList
|
||||
},
|
||||
// 上一页
|
||||
prev() {
|
||||
if (this.async) {
|
||||
// 异步获取数据
|
||||
this.disabledPre = true
|
||||
this.asyncSearchFlag && this.asyncSearch
|
||||
? this.$emit(
|
||||
'get-data-by-keyword',
|
||||
this.searchWord,
|
||||
this.asyncSearchPageIndex <= 1 ? 1 : --this.asyncSearchPageIndex
|
||||
)
|
||||
: this.$emit(
|
||||
'get-data',
|
||||
this.asyncPageIndex <= 1 ? 1 : --this.asyncPageIndex
|
||||
)
|
||||
} else {
|
||||
this.pageIndex > 0 && --this.pageIndex
|
||||
this.pageData()
|
||||
}
|
||||
},
|
||||
// 下一页
|
||||
next() {
|
||||
if (this.async) {
|
||||
// 异步获取数据
|
||||
this.disabledNex = true
|
||||
this.asyncSearchFlag && this.asyncSearch
|
||||
? this.$emit(
|
||||
'get-data-by-keyword',
|
||||
this.searchWord,
|
||||
++this.asyncSearchPageIndex
|
||||
)
|
||||
: this.$emit('get-data', ++this.asyncPageIndex)
|
||||
} else {
|
||||
this.pageIndex <= this.total - 1 && ++this.pageIndex
|
||||
this.pageData()
|
||||
}
|
||||
},
|
||||
// 单选
|
||||
handleCheckedChange(value) {
|
||||
const checkedCount = value.length
|
||||
this.checkAll = checkedCount === this.districtListMock.length
|
||||
this.isIndeterminate =
|
||||
checkedCount > 0 && checkedCount < this.districtListMock.length
|
||||
// 子传父
|
||||
this.$emit('check-district', value)
|
||||
},
|
||||
// 全选
|
||||
handleCheckAllChange(val) {
|
||||
this.checkedData = val ? this.districtListMock.filter(val => !val.disabled).map((val) => val) : []
|
||||
this.isIndeterminate = false
|
||||
// 子传父
|
||||
this.$emit('check-district', this.checkedData)
|
||||
},
|
||||
clearInp() {
|
||||
this.$emit('clear-input')
|
||||
},
|
||||
filterHighlight(label) {
|
||||
const filterWord = this.searchWord.trim()
|
||||
label = label && label.trim()
|
||||
if (filterWord && label) {
|
||||
const reg = new RegExp(filterWord)
|
||||
return label.replace(reg, `<span style="color: ${this.highlightColor}">${filterWord}</span>`)
|
||||
} else {
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.district-panel {
|
||||
width: 280px;
|
||||
|
||||
.el-transfer-panel__header {
|
||||
.el-checkbox {
|
||||
display: inline-block;
|
||||
|
||||
>>> .el-checkbox__label {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-transfer-panel__body {
|
||||
height: 335px;
|
||||
//padding: 6px 0;
|
||||
|
||||
.el-transfer-panel__filter {
|
||||
margin: 6px 14px;
|
||||
line-height: 0;
|
||||
|
||||
.showClear {
|
||||
padding-right: 30px;
|
||||
border-radius: 0;
|
||||
}
|
||||
.clear-input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
transition: all 0.3s;
|
||||
line-height: 33px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.clear-input {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-checkbox-group {
|
||||
height: 295px;
|
||||
overflow: auto;
|
||||
&.expand {
|
||||
height: 290px;
|
||||
}
|
||||
|
||||
.el-transfer-panel__item {
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
|
||||
>>> .el-checkbox__label {
|
||||
font-weight: 400;
|
||||
line-height: 28px ;
|
||||
}
|
||||
|
||||
>>> .el-checkbox__input {
|
||||
top: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-number {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 0;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.no-data {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 6px 15px 0;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
.vip-footer {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid #ebeef5;
|
||||
|
||||
.v-page {
|
||||
width: 50%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 15px;
|
||||
&:first-child {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -52,7 +52,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
empty: () => {}
|
||||
empty: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<TagSearch :options="iOption" v-bind="$attrs" v-on="$listeners" />
|
||||
<span>
|
||||
<el-button v-if="shouldFold" circle class="search-btn" size="mini" @click="handleManualSearch">
|
||||
<svg-icon icon-class="search" />
|
||||
</el-button>
|
||||
<TagSearch v-else :options="iOption" v-bind="$attrs" v-on="$listeners" @tag-search="handleTagSearch" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -25,17 +30,27 @@ export default {
|
||||
exclude: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 建议折叠
|
||||
fold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalOptions: []
|
||||
internalOptions: [],
|
||||
tags: [],
|
||||
manualSearch: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iOption() {
|
||||
const options = this.options.concat(this.internalOptions)
|
||||
return _.uniqWith(options, _.isEqual)
|
||||
},
|
||||
shouldFold() {
|
||||
return this.fold && this.tags.length === 0 && !this.manualSearch
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -52,6 +67,19 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTagSearch(tags) {
|
||||
if (_.isEqual(tags, this.tags)) {
|
||||
return
|
||||
}
|
||||
this.tags = tags
|
||||
if (tags.length === 0) {
|
||||
this.manualSearch = false
|
||||
}
|
||||
this.$emit('tagSearch', tags)
|
||||
},
|
||||
handleManualSearch() {
|
||||
this.manualSearch = true
|
||||
},
|
||||
async genericOptions() {
|
||||
const vm = this // 透传This
|
||||
vm.internalOptions = [] // 重置
|
||||
@@ -102,4 +130,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
.search-btn {
|
||||
margin-top: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { newURL, replaceAllUUID } from '@/utils/common'
|
||||
import ColumnSettingPopover from './components/ColumnSettingPopover.vue'
|
||||
import LabelsFormatter from '@/components/Table/TableFormatters/LabelsFormatter.vue'
|
||||
|
||||
export default {
|
||||
name: 'AutoDataTable',
|
||||
@@ -140,6 +141,9 @@ export default {
|
||||
case 'date_start':
|
||||
col.formatter = DateFormatter
|
||||
break
|
||||
case 'labels':
|
||||
col.formatter = LabelsFormatter
|
||||
break
|
||||
case 'comment':
|
||||
col.showOverflowTooltip = true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<slot name="no-data" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<!--
|
||||
过滤 selection 相关事件的透传,避免父组件收到 el-table 原生的"当前页" selection,
|
||||
导致跨页选择(persistSelection)被覆盖,只剩当页数据。
|
||||
选择事件统一走 selectStrategy,在内部维护全量 selected 并向外 emit。
|
||||
-->
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
@@ -12,7 +17,7 @@
|
||||
:row-class-name="rowClassName"
|
||||
v-bind="tableAttrs"
|
||||
@select="selectStrategy.onSelect"
|
||||
v-on="$listeners"
|
||||
v-on="forwardListeners"
|
||||
@selection-change="selectStrategy.onSelectionChange"
|
||||
@select-all="selectStrategy.onSelectAll($event, canSelect)"
|
||||
@sort-change="onSortChange"
|
||||
@@ -803,6 +808,16 @@ export default {
|
||||
selectStrategy() {
|
||||
return getSelectStrategy(this)
|
||||
},
|
||||
// 过滤会与内部选择策略冲突的事件,避免父组件只拿到当前页 selection
|
||||
forwardListeners() {
|
||||
const listeners = { ...this.$listeners }
|
||||
delete listeners['selection-change']
|
||||
delete listeners['select']
|
||||
delete listeners['select-all']
|
||||
// 外层如需监听 selection 变化,请监听本组件透出的 selection-change,
|
||||
// 该事件来自选择策略,已汇总跨页后的全量 selected
|
||||
return listeners
|
||||
},
|
||||
searchLocatedSlotKeys() {
|
||||
return getLocatedSlotKeys(this.$slots, 'search:')
|
||||
},
|
||||
|
||||
@@ -13,15 +13,28 @@ class StrategyAbstract {
|
||||
this.onSelect = this.onSelect.bind(this)
|
||||
this.onSelectAll = this.onSelectAll.bind(this)
|
||||
}
|
||||
|
||||
get elTable() {
|
||||
return this.elDataTable.$refs.table
|
||||
}
|
||||
onSelectionChange() {}
|
||||
onSelect() {}
|
||||
onSelectAll() {}
|
||||
toggleRowSelection() {}
|
||||
clearSelection() {}
|
||||
updateElTableSelection() {}
|
||||
|
||||
onSelectionChange() {
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
}
|
||||
|
||||
onSelectAll() {
|
||||
}
|
||||
|
||||
toggleRowSelection() {
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
}
|
||||
|
||||
updateElTableSelection() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,14 +47,16 @@ class StrategyNormal extends StrategyAbstract {
|
||||
onSelectionChange(val) {
|
||||
this.elDataTable.selected = val
|
||||
}
|
||||
|
||||
/**
|
||||
* toggleRowSelection和clearSelection的表现与el-table一致
|
||||
*/
|
||||
toggleRowSelection(...args) {
|
||||
return this.elTable.toggleRowSelection(...args)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
return this.elTable.clearSelection()
|
||||
return this.elTable?.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,18 +79,59 @@ class StrategyPersistSelection extends StrategyAbstract {
|
||||
onSelect(selection, row) {
|
||||
const isChosen = selection.indexOf(row) > -1
|
||||
this.toggleRowSelection(row, isChosen)
|
||||
// el-table 原生 selection-change 仅包含当前页。为保证跨页勾选有效,
|
||||
// 在内部策略维护完 selected 后,向外部同步"全量已选"。
|
||||
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户切换当前页的多选
|
||||
*/
|
||||
onSelectAll(selection, selectable = () => true) {
|
||||
const isSelected = !!selection.length
|
||||
this.elDataTable.data.forEach(r => {
|
||||
if (selectable(r)) {
|
||||
this.toggleRowSelection(r, isSelected)
|
||||
}
|
||||
})
|
||||
const { id, selected, data } = this.elDataTable
|
||||
const selectableRows = data.filter(selectable)
|
||||
// const isSelected = !!selection.length
|
||||
|
||||
// 创建已选择项的 id 集合,用于快速查找
|
||||
const selectedIds = new Set(selected.map(r => r[id]))
|
||||
const currentPageIds = new Set(selectableRows.map(row => row[id]))
|
||||
|
||||
// 前页面的选中状态
|
||||
const currentPageSelectedCount = selectableRows.filter(row =>
|
||||
selectedIds.has(row[id])
|
||||
).length
|
||||
|
||||
// 判断是全选还是取消全选
|
||||
const shouldSelectAll = currentPageSelectedCount < selectableRows.length
|
||||
|
||||
this.elTable?.clearSelection()
|
||||
|
||||
if (shouldSelectAll) {
|
||||
selectableRows.forEach(row => {
|
||||
if (!selectedIds.has(row[id])) selected.push(row)
|
||||
|
||||
this.elTable.toggleRowSelection(row, true)
|
||||
|
||||
// ! 这里需要触发事件,否则在 el-table 中无法触发 selection-change 事件
|
||||
this.elDataTable.$emit('toggle-row-selection', true, row)
|
||||
})
|
||||
} else {
|
||||
const newSelected = []
|
||||
|
||||
selected.forEach(row => {
|
||||
if (!currentPageIds.has(row[id])) {
|
||||
newSelected.push(row)
|
||||
} else {
|
||||
this.elDataTable.$emit('toggle-row-selection', false, row)
|
||||
}
|
||||
})
|
||||
|
||||
this.elDataTable.selected = newSelected
|
||||
}
|
||||
|
||||
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
|
||||
}
|
||||
|
||||
/**
|
||||
* toggleRowSelection和clearSelection管理elDataTable的selected数组
|
||||
* 记得最后要将状态同步到el-table中
|
||||
@@ -83,32 +139,46 @@ class StrategyPersistSelection extends StrategyAbstract {
|
||||
toggleRowSelection(row, isSelected) {
|
||||
const { id, selected } = this.elDataTable
|
||||
const foundIndex = selected.findIndex(r => r[id] === row[id])
|
||||
|
||||
if (typeof isSelected === 'undefined') {
|
||||
isSelected = foundIndex <= -1
|
||||
}
|
||||
|
||||
if (isSelected && foundIndex === -1) {
|
||||
selected.push(row)
|
||||
} else if (!isSelected && foundIndex > -1) {
|
||||
selected.splice(foundIndex, 1)
|
||||
}
|
||||
|
||||
this.elDataTable.$emit('toggle-row-selection', isSelected, row)
|
||||
this.updateElTableSelection()
|
||||
// 切换后同步全量 selection(跨页)
|
||||
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.elDataTable.selected = []
|
||||
this.updateElTableSelection()
|
||||
// 清空后也同步给外部,保持外层状态一致
|
||||
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将selected状态同步到el-table中
|
||||
*/
|
||||
updateElTableSelection() {
|
||||
const { data, id, selected } = this.elDataTable
|
||||
data.forEach(r => {
|
||||
const isSelected = !!selected.find(r2 => r[id] === r2[id])
|
||||
if (!this.elTable) {
|
||||
return
|
||||
const selectedIds = new Set(selected.map(r => r[id]))
|
||||
|
||||
this.elTable?.clearSelection()
|
||||
|
||||
data.forEach(row => {
|
||||
const shouldBeSelected = selectedIds.has(row[id])
|
||||
if (!this.elTable) return
|
||||
|
||||
if (shouldBeSelected) {
|
||||
this.elTable.toggleRowSelection(row, true)
|
||||
}
|
||||
this.elTable.toggleRowSelection(r, isSelected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ export default {
|
||||
this.toggleRowSelection(row, true)
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('loaded')
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
localStorage.setItem('paginationSize', val)
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="mfaDialogShow">
|
||||
<UserConfirmDialog
|
||||
:url="url"
|
||||
@AuthMFAError="handleAuthMFAError"
|
||||
@UserConfirmCancel="handleExportCancel"
|
||||
@UserConfirmDone="showExportDialog"
|
||||
/>
|
||||
</div>
|
||||
<Dialog
|
||||
v-if="exportDialogShow"
|
||||
:destroy-on-close="true"
|
||||
:title="$tc('common.Export')"
|
||||
:visible.sync="exportDialogShow"
|
||||
width="70%"
|
||||
width="700px"
|
||||
@cancel="handleExportCancel()"
|
||||
@confirm="handleExportConfirm()"
|
||||
>
|
||||
@@ -29,7 +21,9 @@
|
||||
:disabled="!option.can"
|
||||
:label="option.value"
|
||||
style="padding: 10px 20px;"
|
||||
>{{ option.label }}</el-radio>
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('common.imExport.ExportRange')" :label-width="'100px'" class="export-form">
|
||||
@@ -40,7 +34,9 @@
|
||||
:disabled="!option.can"
|
||||
:label="option.value"
|
||||
class="export-item"
|
||||
>{{ option.label }}</el-radio>
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -50,15 +46,14 @@
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import * as queryUtil from '@/components/Table/DataTable/compenents/el-data-table/utils/query'
|
||||
import { download } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'ExportDialog',
|
||||
components: {
|
||||
Dialog,
|
||||
UserConfirmDialog
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
selectedRows: {
|
||||
@@ -107,12 +102,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: {},
|
||||
exportDialogShow: false,
|
||||
exportOption: 'all',
|
||||
exportTypeOption: 'csv',
|
||||
meta: {},
|
||||
mfaVerified: false,
|
||||
mfaDialogShow: false
|
||||
mfaDialogShow: false,
|
||||
confirmUrl: '/api/v1/accounts/account-secrets/?limit=1'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -136,7 +131,6 @@ export default {
|
||||
delete query[key]
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
},
|
||||
tableHasQuery() {
|
||||
@@ -189,18 +183,12 @@ export default {
|
||||
this.exportDialogShow = true
|
||||
return
|
||||
}
|
||||
// 这是需要校验 MFA 的
|
||||
if (!this.mfaDialogShow) {
|
||||
this.mfaDialogShow = true
|
||||
} else {
|
||||
this.$axios.get('/api/v1/authentication/confirm/check/?confirm_type=mfa').then(() => {
|
||||
this.exportDialogShow = true
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadCsv(url) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
download(url)
|
||||
},
|
||||
async defaultPerformExport(selectRows, exportOption, q, exportTypeOption) {
|
||||
const url = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
|
||||
@@ -231,13 +219,11 @@ export default {
|
||||
async handleExportConfirm() {
|
||||
await this.handleExport()
|
||||
this.exportDialogShow = false
|
||||
this.mfaDialogShow = false
|
||||
},
|
||||
handleExportCancel() {
|
||||
const vm = this
|
||||
setTimeout(() => {
|
||||
vm.exportDialogShow = false
|
||||
vm.mfaDialogShow = false
|
||||
}, 100)
|
||||
},
|
||||
handleAuthMFAError() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:title="importTitle"
|
||||
:visible.sync="showImportDialog"
|
||||
class="importDialog"
|
||||
width="70%"
|
||||
width="900px"
|
||||
@close="handleImportCancel"
|
||||
>
|
||||
<el-form v-if="!showTable" label-position="left" style="padding-left: 20px">
|
||||
@@ -68,7 +68,7 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import ImportTable from '@/components/Table/ListTable/TableAction/ImportTable.vue'
|
||||
import { getErrorResponseMsg } from '@/utils/common'
|
||||
import { download, getErrorResponseMsg } from '@/utils/common'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
|
||||
export default {
|
||||
@@ -199,7 +199,8 @@ export default {
|
||||
},
|
||||
async getDownloadTemplateUrl(tp) {
|
||||
const template = this.importOption === 'create' ? 'import' : 'update'
|
||||
let query = `format=${tp}&template=${template}`
|
||||
const action = this.importOption === 'create' ? 'create' : 'partial_update'
|
||||
let query = `format=${tp}&template=${template}&action=${action}`
|
||||
if (this.importOption === 'update' && this.selectedRows.length > 0) {
|
||||
const resources = []
|
||||
for (const item of this.selectedRows) {
|
||||
@@ -220,10 +221,7 @@ export default {
|
||||
this.$message.success(msg)
|
||||
},
|
||||
downloadCsv(url) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
download(url)
|
||||
},
|
||||
async handleImportConfirm() {
|
||||
await this.$refs['importTable'].performUpload()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-row type="flex" align="center">
|
||||
<el-col :md="8" :sm="24">
|
||||
<div class="tableFilter">
|
||||
<el-radio-group v-model="importStatusFilter" size="small">
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="8" :sm="24" style="text-align: center">
|
||||
<el-col :md="16" :sm="24" style="text-align: center; display: flex; align-items: 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>
|
||||
|
||||
187
src/components/Table/ListTable/TableAction/LabelSearch.vue
Normal file
187
src/components/Table/ListTable/TableAction/LabelSearch.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="label-search">
|
||||
<el-button
|
||||
v-if="!showLabelSearch"
|
||||
class="label-button"
|
||||
size="small"
|
||||
@click="showSearchSelect"
|
||||
>
|
||||
<svg-icon icon-class="tag" />
|
||||
<span>{{ $t('common.Label') }}</span>
|
||||
</el-button>
|
||||
<el-cascader
|
||||
v-else
|
||||
ref="labelCascader"
|
||||
v-model="labelValue"
|
||||
:options="labelOptions"
|
||||
:placeholder="placeholder"
|
||||
:props="labelProps"
|
||||
class="label-cascader"
|
||||
clearable
|
||||
filterable
|
||||
separator=": "
|
||||
size="small"
|
||||
@focus="handleCascaderFocus"
|
||||
@visible-change="handleCascaderVisibleChange"
|
||||
>
|
||||
<template slot-scope="{ node, data }">
|
||||
<span>{{ data.label }}</span>
|
||||
<span v-if="!node.isLeaf"> ({{ data.children.length -1 }}) </span>
|
||||
</template>
|
||||
<i slot="prefix" class="el-input__icon el-icon-search" />
|
||||
</el-cascader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'LabelSearch',
|
||||
data() {
|
||||
return {
|
||||
showLabelSearch: false,
|
||||
labelProps: {
|
||||
multiple: true
|
||||
},
|
||||
labelOptions: [],
|
||||
labelValue: [],
|
||||
placeholder: this.$t('labels.SelectLabelFilter')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
labelValue(newValue) {
|
||||
if (!newValue || newValue.length === 0) {
|
||||
this.showLabelSearch = false
|
||||
}
|
||||
|
||||
if (!newValue || newValue.length === 0) {
|
||||
this.$emit('labelSearch', '')
|
||||
return
|
||||
}
|
||||
|
||||
const labelSearch = newValue.map(item => item.join(':')).join(',')
|
||||
this.$emit('labelSearch', labelSearch)
|
||||
},
|
||||
showLabelSearch(newValue) {
|
||||
this.$emit('showLabelSearch', newValue)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('labelSearch', label => {
|
||||
if (!label) {
|
||||
this.labelValue = []
|
||||
this.showLabelSearch = true
|
||||
return
|
||||
}
|
||||
const labels = label.split(',').map(item => item.split(':'))
|
||||
const notExistLabels = labels.filter(item => {
|
||||
return !this.labelValue.find(label => label[0] === item[0] && label[1] === item[1])
|
||||
})
|
||||
this.labelValue = [...this.labelValue, ...notExistLabels]
|
||||
this.getLabelOptions()
|
||||
setTimeout(() => {
|
||||
this.showLabelSearch = true
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.$eventBus.$off('labelSearch')
|
||||
},
|
||||
methods: {
|
||||
handleCascaderFocus() {
|
||||
this.setSearchFocus()
|
||||
},
|
||||
handleCascaderVisibleChange(visible) {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.$refs.labelCascader.updateStyle()
|
||||
},)
|
||||
return
|
||||
} else {
|
||||
const input = this.$refs.labelCascader.$el.getElementsByClassName('el-input--suffix')[0].querySelector('input')
|
||||
input.style.height = '34px'
|
||||
}
|
||||
if (this.labelValue.length === 0) {
|
||||
this.showLabelSearch = false
|
||||
}
|
||||
},
|
||||
getLabelOptions() {
|
||||
if (this.labelOptions.length > 0) {
|
||||
return
|
||||
}
|
||||
const url = '/api/v1/labels/labels/'
|
||||
this.$axios.get(url).then(data => {
|
||||
const groupedLabelOptions = _.groupBy(data, 'name')
|
||||
const labelOptions = []
|
||||
for (const [key, labels] of Object.entries(groupedLabelOptions)) {
|
||||
const all = { value: '*', label: this.$t('common.All') }
|
||||
const children = _.sortBy(labels, 'value').map(label => ({
|
||||
value: label.value,
|
||||
label: label.value
|
||||
}))
|
||||
labelOptions.push({
|
||||
value: key,
|
||||
label: key,
|
||||
children: [all, ...children]
|
||||
})
|
||||
}
|
||||
this.labelOptions = _.sortBy(labelOptions, 'label')
|
||||
})
|
||||
},
|
||||
setSearchFocus() {
|
||||
setTimeout(() => {
|
||||
this.$refs.labelCascader.$el.getElementsByClassName('el-cascader__search-input')[0].focus()
|
||||
}, 100)
|
||||
},
|
||||
showSearchSelect() {
|
||||
this.getLabelOptions()
|
||||
this.showLabelSearch = true
|
||||
setTimeout(() => {
|
||||
this.$refs.labelCascader.toggleDropDownVisible(true)
|
||||
this.setSearchFocus()
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.label-search {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.label-button {
|
||||
padding: 10px 13px 10px 12px;
|
||||
}
|
||||
|
||||
.label-select {
|
||||
}
|
||||
|
||||
.label-cascader {
|
||||
width: 300px;
|
||||
>>> .el-input--suffix.el-input {
|
||||
input {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
>>> .el-input__inner {
|
||||
font-size: 13px;
|
||||
}
|
||||
>>> .el-cascader__search-input {
|
||||
display: none;
|
||||
margin: 0 0 2px 13px;
|
||||
}
|
||||
>>> .el-input.is-focus + .el-cascader__tags .el-cascader__search-input {
|
||||
display: inline;
|
||||
}
|
||||
>>> .el-input.is-focus + .el-cascader__tags {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
>>> .el-cascader__tags {
|
||||
white-space: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
title: this.$t('common.BatchUpdate'),
|
||||
name: 'actionUpdateSelected',
|
||||
has: this.hasBulkUpdate,
|
||||
icon: 'fa fa-refresh',
|
||||
fa: 'batch-update',
|
||||
can: function({ selectedRows }) {
|
||||
let canBulkUpdate = vm.canBulkUpdate
|
||||
if (typeof canBulkUpdate === 'function') {
|
||||
|
||||
@@ -122,8 +122,13 @@ export default {
|
||||
})
|
||||
},
|
||||
iExportOptions() {
|
||||
const options = assignIfNot(this.exportOptions, { url: this.tableUrl })
|
||||
return options
|
||||
// const options = assignIfNot(this.exportOptions, { url: this.tableUrl })
|
||||
// return options
|
||||
|
||||
return {
|
||||
url: this.tableUrl,
|
||||
...this.exportOptions
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<div :class="searchClass" class="search">
|
||||
<LabelSearch
|
||||
v-if="hasLabelSearch"
|
||||
@labelSearch="handleLabelSearch"
|
||||
@showLabelSearch="handleLabelSearchShowChange"
|
||||
/>
|
||||
<AutoDataSearch
|
||||
v-if="hasSearch"
|
||||
:fold="foldSearch"
|
||||
class="right-side-item action-search"
|
||||
v-bind="iSearchTableConfig"
|
||||
@tagSearch="handleTagSearch"
|
||||
@@ -41,12 +47,14 @@ import RightSide from './RightSide.vue'
|
||||
import AutoDataSearch from '@/components/Table/AutoDataSearch/index.vue'
|
||||
import DatetimeRangePicker from '@/components/Form/FormFields/DatetimeRangePicker.vue'
|
||||
import { getDaysAgo, getDaysFuture } from '@/utils/common'
|
||||
import LabelSearch from '@/components/Table/ListTable/TableAction/LabelSearch.vue'
|
||||
|
||||
const defaultTrue = { type: Boolean, default: true }
|
||||
const defaultFalse = { type: Boolean, default: false }
|
||||
export default {
|
||||
name: 'TableAction',
|
||||
components: {
|
||||
LabelSearch,
|
||||
LeftSide,
|
||||
RightSide,
|
||||
AutoDataSearch,
|
||||
@@ -57,6 +65,7 @@ export default {
|
||||
hasSearch: defaultTrue,
|
||||
hasRightActions: defaultTrue,
|
||||
hasDatePicker: defaultFalse,
|
||||
hasLabelSearch: defaultFalse,
|
||||
datePicker: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
@@ -89,7 +98,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
keyword: ''
|
||||
keyword: '',
|
||||
foldSearch: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -112,124 +122,146 @@ export default {
|
||||
return this.hasLeftActions ? 'right' : 'left'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('done')
|
||||
},
|
||||
methods: {
|
||||
handleTagSearch(val) {
|
||||
this.searchTable(val)
|
||||
},
|
||||
handleDateChange(val) {
|
||||
this.datePick(val)
|
||||
},
|
||||
handleLabelSearch(val) {
|
||||
if (!val || val.length === 0) {
|
||||
this.searchTable({ labels: '' })
|
||||
return
|
||||
}
|
||||
this.searchTable({ labels: val })
|
||||
},
|
||||
handleLabelSearchShowChange(val) {
|
||||
this.foldSearch = val
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-header {
|
||||
/*display: flex;*/
|
||||
/*flex-direction: row;*/
|
||||
/*justify-content: space-between;*/
|
||||
}
|
||||
<style lang='scss' scoped>
|
||||
.table-header {
|
||||
/*display: flex;*/
|
||||
/*flex-direction: row;*/
|
||||
/*justify-content: space-between;*/
|
||||
}
|
||||
|
||||
.right-side-item {
|
||||
}
|
||||
.right-side-item {
|
||||
}
|
||||
|
||||
.right-side-actions >>> .el-button {
|
||||
border: none;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #888;
|
||||
background-color: transparent;
|
||||
}
|
||||
.right-side-actions > > > .el-button {
|
||||
border: none;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #888;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.right-side-actions >>> .fa {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.right-side-actions > > > .fa {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.right-side-actions >>> .el-button:hover {
|
||||
background-color: rgb(0, 0, 0, 0.05);
|
||||
}
|
||||
.right-side-actions > > > .el-button:hover {
|
||||
background-color: rgb(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.action-search >>> .el-input__suffix i {
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
}
|
||||
.action-search > > > .el-input__suffix i {
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.action-search >>> .el-cascader {
|
||||
line-height: 32px !important;
|
||||
}
|
||||
.action-search > > > .el-cascader {
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
.right-side-actions {
|
||||
display: flex;
|
||||
padding-left: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.right-side-actions {
|
||||
display: flex;
|
||||
padding-left: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-action-right-side {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.table-action-right-side {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.export-item {
|
||||
display: block;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
.export-item {
|
||||
display: block;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.datepicker {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
line-height: 32px;
|
||||
}
|
||||
.table-header {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.left-side {
|
||||
float: left;
|
||||
display: block;
|
||||
}
|
||||
.left-side {
|
||||
float: left;
|
||||
display: block;
|
||||
|
||||
.right-side {
|
||||
float: right;
|
||||
}
|
||||
& > > > .action-item.el-dropdown {
|
||||
height: 33px;
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
& > .el-button {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile .search {
|
||||
display: inherit;
|
||||
}
|
||||
.right-side {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mobile .search .datepicker {
|
||||
margin-left: 0;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search.left {
|
||||
float: left;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.mobile .search {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.search.right {
|
||||
float: right;
|
||||
}
|
||||
.mobile .search .datepicker {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mobile .search.right {
|
||||
float: none;
|
||||
}
|
||||
.search.left {
|
||||
float: left;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mobile .search.right .action-search {
|
||||
width: 100%;
|
||||
}
|
||||
.search.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mobile .right-side {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.mobile .search.right {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.mobile .search.right .action-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile .right-side {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.filter-field.right-side-item.action-search {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.filter-field.right-side-item.action-search {
|
||||
height: 34px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
:selected-rows="selectedRows"
|
||||
:table-url="tableUrl"
|
||||
v-bind="iHeaderActions"
|
||||
@done="handleActionInitialDone"
|
||||
/>
|
||||
<IBox class="table-content">
|
||||
<AutoDataTable
|
||||
v-if="actionInit"
|
||||
ref="dataTable"
|
||||
:config="iTableConfig"
|
||||
:filter-table="filter"
|
||||
@@ -60,7 +62,7 @@ export default {
|
||||
extraQuery = {
|
||||
...extraQuery,
|
||||
date_from: getDaysAgo(7).toISOString(),
|
||||
date_to: getDayEnd().toISOString()
|
||||
date_to: this.$moment(getDayEnd()).add(1, 'day').toISOString()
|
||||
}
|
||||
this.headerActions.datePicker = Object.assign({
|
||||
dateStart: extraQuery.date_from,
|
||||
@@ -73,7 +75,10 @@ export default {
|
||||
return {
|
||||
selectedRows: [],
|
||||
init: false,
|
||||
extraQuery: extraQuery
|
||||
isDeactivated: false,
|
||||
extraQuery: extraQuery,
|
||||
actionInit: this.headerActions.has === false,
|
||||
initQuery: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -166,19 +171,43 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleActionInitialDone() {
|
||||
setTimeout(() => {
|
||||
this.actionInit = true
|
||||
}, 100)
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
this.selectedRows = val
|
||||
},
|
||||
reloadTable() {
|
||||
this.dataTable.getList()
|
||||
},
|
||||
updateInitQuery(attrs) {
|
||||
if (!this.actionInit) {
|
||||
this.initQuery = attrs
|
||||
for (const key in attrs) {
|
||||
this.$set(this.extraQuery, key, attrs[key])
|
||||
}
|
||||
return true
|
||||
}
|
||||
const removeKeys = Object.keys(this.initQuery).filter(key => !attrs[key])
|
||||
for (const key of removeKeys) {
|
||||
this.$delete(this.extraQuery, key)
|
||||
}
|
||||
},
|
||||
search(attrs) {
|
||||
this.$log.debug('ListTable: search table', attrs)
|
||||
const init = this.updateInitQuery(attrs)
|
||||
if (init) {
|
||||
return
|
||||
}
|
||||
this.$emit('TagSearch', attrs)
|
||||
return this.dataTable?.search(attrs, true)
|
||||
this.$refs.dataTable?.$refs.dataTable?.search(attrs, true)
|
||||
},
|
||||
filter(attrs) {
|
||||
this.$emit('TagFilter', attrs)
|
||||
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
|
||||
this.$log.debug('ListTable: found filter change', attrs)
|
||||
this.search(attrs)
|
||||
},
|
||||
hasActionPerm(action) {
|
||||
const permRequired = this.permissions[action]
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<el-popover
|
||||
:title="title"
|
||||
placement="left-start"
|
||||
trigger="click"
|
||||
@show="getAsyncItems"
|
||||
>
|
||||
<div class="detail-content">
|
||||
<div v-for="account of accountData" :key="account.id" class="detail-item">
|
||||
<span>{{ account.name }}({{ account.username }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="primary">{{ $t('common.View') }}</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
|
||||
export default {
|
||||
name: 'SwitchFormatter',
|
||||
extends: BaseFormatter,
|
||||
data() {
|
||||
return {
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
|
||||
value: this.cellValue,
|
||||
accountData: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.formatterArgs.title || this.col.label
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getAsyncItems() {
|
||||
const userId = this.$route.params.id
|
||||
const url = `/api/v1/perms/users/${userId}/assets/${this.row.id}`
|
||||
this.$axios.get(url).then(res => {
|
||||
this.accountData = res?.permed_accounts || []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-content {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,6 @@ const defaultUpdateCallback = function({ row, col }) {
|
||||
const id = row.id
|
||||
let route = { params: { id: id }}
|
||||
const updateRoute = this.colActions.updateRoute
|
||||
console.log('Update route: ', updateRoute)
|
||||
|
||||
if (typeof updateRoute === 'object') {
|
||||
route = Object.assign(route, updateRoute)
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
<template>
|
||||
<el-popover
|
||||
:disabled="!showItems"
|
||||
:open-delay="500"
|
||||
:title="title"
|
||||
placement="top-start"
|
||||
trigger="hover"
|
||||
width="400"
|
||||
@show="getAsyncItems"
|
||||
>
|
||||
<div class="detail-content">
|
||||
<div v-for="item of items" :key="getKey(item)" class="detail-item">
|
||||
<span class="detail-item-name">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="reference">{{ items && items.length }}</span>
|
||||
<span slot="reference">{{ amount }}</span>
|
||||
</el-popover>
|
||||
</template>
|
||||
</DetailFormatter>
|
||||
@@ -37,52 +39,111 @@ export default {
|
||||
showItems: true,
|
||||
getItem(item) {
|
||||
return item.name
|
||||
}
|
||||
},
|
||||
async: false,
|
||||
ajax: {},
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs || {})
|
||||
return {
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs || {})
|
||||
formatterArgs: formatterArgs,
|
||||
data: formatterArgs.async ? [] : (this.cellValue || []),
|
||||
amount: '',
|
||||
asyncGetDone: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.formatterArgs.title || ''
|
||||
return this.formatterArgs.title || this.col.label.replace('amount', '').replace('数量', '')
|
||||
},
|
||||
cellValueToRemove() {
|
||||
return this.formatterArgs.cellValueToRemove || []
|
||||
},
|
||||
items() {
|
||||
if (this.formatterArgs.async && !this.asyncGetDone) {
|
||||
return [this.$t('common.tree.Loading') + '...']
|
||||
}
|
||||
const getItem = this.formatterArgs.getItem || (item => item.name)
|
||||
let data = this.cellValue?.map(item => getItem(item)) || []
|
||||
let data = []
|
||||
if (Array.isArray(this.data)) {
|
||||
data = this.data.map(item => getItem(item)) || []
|
||||
} else {
|
||||
// object {key: [value]}
|
||||
data = Object.entries(this.data).map(([key, value]) => {
|
||||
const item = { key: key, value: value }
|
||||
return getItem(item)
|
||||
}) || []
|
||||
}
|
||||
data = data.filter(Boolean)
|
||||
return data
|
||||
},
|
||||
showItems() {
|
||||
return this.formatterArgs.showItems !== false && this.cellValue?.length > 0
|
||||
return this.amount !== 0 && this.amount !== ''
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (this.formatterArgs.async) {
|
||||
this.amount = this.cellValue
|
||||
} else {
|
||||
let cellValue = []
|
||||
if (Array.isArray(this.cellValue)) {
|
||||
cellValue = this.cellValue
|
||||
} else {
|
||||
// object {key: [value]}
|
||||
cellValue = Object.keys(this.cellValue)
|
||||
}
|
||||
this.amount = (cellValue?.filter(value => !this.cellValueToRemove.includes(value)) || []).length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getKey(item) {
|
||||
const id = Math.random().toString(36).substring(2)
|
||||
const id = Math.random().toString(36).substring(16)
|
||||
return id + item
|
||||
},
|
||||
getDefaultUrl() {
|
||||
const url = new URL(this.url, location.origin)
|
||||
url.pathname += this.row.id + '/'
|
||||
return url.pathname
|
||||
},
|
||||
async getAsyncItems() {
|
||||
if (!this.formatterArgs.async) {
|
||||
return
|
||||
}
|
||||
if (this.asyncGetDone) {
|
||||
return
|
||||
}
|
||||
const url = this.formatterArgs.ajax.url || this.getDefaultUrl()
|
||||
const params = this.formatterArgs.ajax.params || {}
|
||||
const transform = this.formatterArgs.ajax.transform || (resp => resp[this.col.prop.replace('_amount', '')])
|
||||
const response = await this.$axios.get(url, { params: params })
|
||||
this.data = transform(response)
|
||||
this.asyncGetDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.detail-content {
|
||||
padding: 10px;
|
||||
padding: 5px 10px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #F5F7FA;
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item:first-child {
|
||||
//border-top: 1px solid #EBEEF5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
<template>
|
||||
<span>{{ value }}</span>
|
||||
<span class="date">{{ dateValue }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'DateFormatter',
|
||||
extends: BaseFormatter,
|
||||
data() {
|
||||
let value
|
||||
if (this.cellValue) {
|
||||
value = toSafeLocalDateStr(this.cellValue)
|
||||
} else {
|
||||
value = ''
|
||||
}
|
||||
// let value
|
||||
// if (this.cellValue) {
|
||||
// value = toSafeLocalDateStr(this.cellValue)
|
||||
// } else {
|
||||
// value = '-'
|
||||
// }
|
||||
// const locale = this.$i18n.locale
|
||||
// const value = dt.toLocaleString(locale, { hourCycle: 'h23' })
|
||||
// debug(this.$i18n.locale)
|
||||
return {
|
||||
value: value
|
||||
}
|
||||
// return {
|
||||
// value: value
|
||||
// }
|
||||
// return {
|
||||
// value: `${year}-${month}-${date} ${hour}:${minutes}:${seconds}`
|
||||
// }
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
dateValue() {
|
||||
if (this.cellValue) {
|
||||
return toSafeLocalDateStr(this.cellValue)
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<el-button
|
||||
ref="deleteButton"
|
||||
:disabled="iDisabled"
|
||||
size="mini"
|
||||
type="danger"
|
||||
:disabled="iDisabled"
|
||||
@click="onDelete(col, row, cellValue, reload)"
|
||||
>
|
||||
<i class="fa fa-minus" />
|
||||
@@ -16,9 +16,18 @@ import BaseFormatter from './base.vue'
|
||||
export default {
|
||||
name: 'DeleteActionFormatter',
|
||||
extends: BaseFormatter,
|
||||
data() {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
return {
|
||||
formatterArgs: formatterArgs
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iDisabled() {
|
||||
// 禁用
|
||||
if (this.formatterArgs.disabled !== undefined) {
|
||||
return this.formatterArgs.disabled
|
||||
}
|
||||
return (this.disabled() || this.$store.getters.currentOrgIsRoot)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:disabled="disabled"
|
||||
:type="col.type || 'info'"
|
||||
class="detail"
|
||||
:class="{ 'clicked': linkClicked }"
|
||||
@click="goDetail"
|
||||
>
|
||||
<slot>
|
||||
@@ -30,6 +31,7 @@ export default {
|
||||
routeQuery: null,
|
||||
can: true,
|
||||
openInNewPage: false,
|
||||
removeColorOnClick: false,
|
||||
getTitle({ col, row, cellValue }) {
|
||||
return cellValue
|
||||
},
|
||||
@@ -43,6 +45,7 @@ export default {
|
||||
data() {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
return {
|
||||
linkClicked: false,
|
||||
formatterArgs: formatterArgs
|
||||
}
|
||||
},
|
||||
@@ -100,6 +103,7 @@ export default {
|
||||
methods: {
|
||||
goDetail() {
|
||||
if (this.formatterArgs.openInNewPage) {
|
||||
this.linkClicked = this.formatterArgs.removeColorOnClick
|
||||
const { href } = this.$router.resolve(this.detailRoute)
|
||||
window.open(href, '_blank')
|
||||
} else {
|
||||
@@ -125,6 +129,11 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.clicked,
|
||||
.el-link.el-link--info.clicked {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
|
||||
export default {
|
||||
name: 'GrantedSystemUsersShowFormatter',
|
||||
extends: BaseFormatter,
|
||||
@@ -28,7 +29,7 @@ export default {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
const url = formatterArgs.getUrl({ row: this.row, col: this.col })
|
||||
const data = await this.$axios.get(url)
|
||||
this.accounts = data.map((item) => item.name)
|
||||
this.accounts = data['permed_accounts'].map((item) => item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
}
|
||||
return text
|
||||
}
|
||||
return '-'
|
||||
return this.items?.distribution || '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
279
src/components/Table/TableFormatters/LabelsFormatter.vue
Normal file
279
src/components/Table/TableFormatters/LabelsFormatter.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="label-container">
|
||||
<a class="label-formatter-col">
|
||||
<span v-if="!iLabels || iLabels.length === 0" style="vertical-align: top;">
|
||||
<el-tag effect="plain" size="mini">
|
||||
<i class="fa fa-tag" /> -
|
||||
</el-tag>
|
||||
</span>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="label of iLabels"
|
||||
:key="label"
|
||||
>
|
||||
<el-tag
|
||||
:type="getLabelType(label)"
|
||||
class="tag-formatter"
|
||||
disable-transitions
|
||||
effect="plain"
|
||||
size="mini"
|
||||
v-bind="formatterArgs.config"
|
||||
@click="handleLabelSearch(label)"
|
||||
>
|
||||
<i class="fa fa-tag" /> <b> {{ getKey(label) }}</b>: {{ getValue(label) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
v-if="formatterArgs.showEditBtn"
|
||||
:class="[{ 'disabled-link': this.$store.getters.currentOrgIsRoot },'edit-btn']"
|
||||
style="padding-left: 5px"
|
||||
@click="showDialog = true"
|
||||
>
|
||||
<i class="fa fa-edit" />
|
||||
</a>
|
||||
<Dialog
|
||||
v-if="showDialog"
|
||||
:title="$tc('labels.BindLabel')"
|
||||
:visible.sync="showDialog"
|
||||
width="600px"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<el-row :gutter="1" class="tag-select">
|
||||
<el-col :span="12">
|
||||
<Select2 v-model="keySelect2.value" v-bind="keySelect2" @change="handleKeyChanged" />
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<Select2
|
||||
v-model="valueSelect2.value"
|
||||
:disabled="!keySelect2.value"
|
||||
style="margin-left: 10px"
|
||||
v-bind="valueSelect2"
|
||||
@change="handleAddLabel"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="tag-zone">
|
||||
<span v-if="!iLabels || iLabels.length === 0"> - </span>
|
||||
<div v-else>
|
||||
<el-tag
|
||||
v-for="label of iLabels"
|
||||
:key="label"
|
||||
:type="getLabelType(label)"
|
||||
class="tag-formatter"
|
||||
closable
|
||||
disable-transitions
|
||||
effect="plain"
|
||||
size="small"
|
||||
v-bind="formatterArgs.config"
|
||||
@close="handleCloseTag(label)"
|
||||
>
|
||||
<i class="fa fa-tag" /> <b>{{ getKey(label) }}</b>: {{ getValue(label) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="tag-tip">
|
||||
<el-link @click="goToLabelList">
|
||||
{{ $t('labels.LabelList') }} <i class="fa fa-external-link" />
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import Dialog from '@/components/Dialog'
|
||||
|
||||
export default {
|
||||
name: 'LabelsFormatter',
|
||||
components: { Select2, Dialog },
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
getLabelType(label) {
|
||||
return 'primary'
|
||||
},
|
||||
getLabels(cellValue) {
|
||||
return cellValue
|
||||
},
|
||||
config: {},
|
||||
showEditBtn: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
focusOn: '',
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
|
||||
initial: [],
|
||||
iLabels: [],
|
||||
keySelect2: {
|
||||
url: '/api/v1/labels/labels/keys/',
|
||||
placeholder: this.$t('labels.SelectKeyOrCreateNew'),
|
||||
// placeholder: this.$t('选择标签键或者创建新的'),
|
||||
allowCreate: true,
|
||||
value: '',
|
||||
multiple: false
|
||||
},
|
||||
valueSelect2: {
|
||||
url: '/api/v1/labels/labels/?name=blank',
|
||||
placeholder: this.$t('labels.SelectValueOrCreateNew'),
|
||||
// placeholder: '选择标签值或者创建新的',
|
||||
allowCreate: true,
|
||||
value: '',
|
||||
multiple: false,
|
||||
ajax: {
|
||||
transformOption: (item) => {
|
||||
return { label: item.value, value: item.value }
|
||||
}
|
||||
}
|
||||
},
|
||||
showDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted() {
|
||||
this.initial = this.formatterArgs.getLabels(this.cellValue)
|
||||
this.iLabels = [...this.initial]
|
||||
},
|
||||
methods: {
|
||||
handleLabelSearch(label) {
|
||||
this.$eventBus.$emit('labelSearch', label)
|
||||
},
|
||||
getLabelType(tag) {
|
||||
return this.formatterArgs.getLabelType(tag)
|
||||
},
|
||||
handleCloseTag(tag) {
|
||||
this.iLabels = this.iLabels.filter(item => item !== tag)
|
||||
},
|
||||
handleKeyChanged(val) {
|
||||
this.valueSelect2.url = `/api/v1/labels/labels/?name=${val}`
|
||||
},
|
||||
getKey(tag) {
|
||||
return tag.split(':')[0]
|
||||
},
|
||||
getValue(tag) {
|
||||
return tag.split(':').slice(1).join(':')
|
||||
},
|
||||
handleAddLabel() {
|
||||
const key = this.keySelect2.value
|
||||
const value = this.valueSelect2.value
|
||||
if (!key || !value) {
|
||||
return
|
||||
}
|
||||
const tag = `${key}:${value}`
|
||||
if (this.iLabels.includes(tag)) {
|
||||
return
|
||||
}
|
||||
this.iLabels.push(tag)
|
||||
this.keySelect2.value = ''
|
||||
this.valueSelect2.value = ''
|
||||
this.$emit('input', this.iLabels)
|
||||
},
|
||||
handleCancel() {
|
||||
this.showDialog = false
|
||||
},
|
||||
handleConfirm() {
|
||||
const origin = _.sortBy(this.initial)
|
||||
const current = _.sortBy(this.iLabels)
|
||||
if (_.isEqual(origin, current)) {
|
||||
return
|
||||
}
|
||||
const path = new URL(this.url, location.origin).pathname
|
||||
const url = `${path}${this.row.id}/`
|
||||
this.$axios.patch(url, { labels: this.iLabels }).then(res => {
|
||||
this.$message.success('修改成功')
|
||||
this.showDialog = false
|
||||
})
|
||||
},
|
||||
goToLabelList() {
|
||||
this.$router.push({ name: 'LabelList' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-select {
|
||||
>>> .el-input__inner::placeholder {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
transition: all 1s;
|
||||
& > i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.label-container {
|
||||
display: flex;
|
||||
.label-formatter-col {
|
||||
overflow: hidden;
|
||||
}
|
||||
&:hover {
|
||||
.edit-btn {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-zone {
|
||||
margin: 20px 0 0 0;
|
||||
border: solid 1px #ebeef5;
|
||||
padding: 10px;
|
||||
background: #f2f2f5;
|
||||
|
||||
.tag-formatter {
|
||||
margin: 1px 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.input-button .el-button.el-button--mini {
|
||||
padding: 5px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.tag-formatter {
|
||||
margin: 2px 0;
|
||||
//display: table;
|
||||
}
|
||||
|
||||
.tag-tip {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.disabled-link {
|
||||
pointer-events: none;
|
||||
color: grey;
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
class="text edit-input"
|
||||
@blur="onEditBlur"
|
||||
/>
|
||||
<span v-if="realValue" class="action">
|
||||
<span class="action">
|
||||
<template v-for="(item, index) in iActions">
|
||||
<el-tooltip
|
||||
v-if="item.has"
|
||||
|
||||
64
src/components/Table/TableFormatters/SwitchFormatter.vue
Normal file
64
src/components/Table/TableFormatters/SwitchFormatter.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="display">
|
||||
<el-switch v-model="value" @change="onChange" />
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
|
||||
export default {
|
||||
name: 'SwitchFormatter',
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
getPatchUrl(row) {
|
||||
return ''
|
||||
},
|
||||
getPatchData(row) {
|
||||
return {}
|
||||
},
|
||||
isDisplay(row) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
|
||||
value: this.cellValue
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
patchUrl() {
|
||||
return this.formatterArgs.getPatchUrl(this.row)
|
||||
},
|
||||
patchData() {
|
||||
return this.formatterArgs.getPatchData(this.row)
|
||||
},
|
||||
display(row) {
|
||||
return this.formatterArgs.isDisplay(this.row)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
this.$axios.patch(this.patchUrl, this.patchData).then(res => {
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).catch(err => {
|
||||
this.value = !val
|
||||
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -4,9 +4,9 @@
|
||||
v-for="tag of iTags"
|
||||
:key="tag"
|
||||
:type="getTagType(tag)"
|
||||
v-bind="formatterArgs.config"
|
||||
class="tag-formatter"
|
||||
disable-transitions
|
||||
v-bind="formatterArgs.config"
|
||||
>
|
||||
<i class="fa fa-tag" /> {{ tag }}
|
||||
</el-tag>
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
|
||||
export default {
|
||||
name: 'TagsFormatter',
|
||||
extends: BaseFormatter,
|
||||
@@ -52,7 +53,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.tag {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -11,10 +11,13 @@ import DialogDetailFormatter from './DialogDetailFormatter.vue'
|
||||
import EditableInputFormatter from './EditableInputFormatter.vue'
|
||||
import StatusFormatter from './StatusFormatter.vue'
|
||||
import TagsFormatter from './TagsFormatter.vue'
|
||||
import LabelsFormatter from './LabelsFormatter.vue'
|
||||
import ObjectRelatedFormatter from './ObjectRelatedFormatter.vue'
|
||||
import TwoTabFormatter from './TwoTabFormatter.vue'
|
||||
import ProtocolsFormatter from './ProtocolsFormatter.vue'
|
||||
import TagChoicesFormatter from './TagChoicesFormatter.vue'
|
||||
import SwitchFormatter from './SwitchFormatter.vue'
|
||||
import AccountInfoFormatter from './AccountInfoFormatter.vue'
|
||||
|
||||
export default {
|
||||
DetailFormatter,
|
||||
@@ -33,7 +36,10 @@ export default {
|
||||
ObjectRelatedFormatter,
|
||||
TwoTabFormatter,
|
||||
ProtocolsFormatter,
|
||||
TagChoicesFormatter
|
||||
TagChoicesFormatter,
|
||||
LabelsFormatter,
|
||||
SwitchFormatter,
|
||||
AccountInfoFormatter
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -53,5 +59,8 @@ export {
|
||||
ObjectRelatedFormatter,
|
||||
TwoTabFormatter,
|
||||
ProtocolsFormatter,
|
||||
TagChoicesFormatter
|
||||
TagChoicesFormatter,
|
||||
LabelsFormatter,
|
||||
SwitchFormatter,
|
||||
AccountInfoFormatter
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user