Compare commits
393 Commits
v4.8
...
v5_refacto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65392f1d0c | ||
|
|
777b139fde | ||
|
|
cccd2cbb7a | ||
|
|
f75a4007f6 | ||
|
|
a9c1e06706 | ||
|
|
b66b779e79 | ||
|
|
841ba521f6 | ||
|
|
d2feaa021e | ||
|
|
c231f66e4a | ||
|
|
7fe6ab5f1b | ||
|
|
c037b52bb6 | ||
|
|
37a45cbc6e | ||
|
|
3d90d56373 | ||
|
|
587f49286f | ||
|
|
5e1b32aaf9 | ||
|
|
e1bc8245c9 | ||
|
|
6b6de2b2c5 | ||
|
|
7913979b4b | ||
|
|
16d8c7c9ac | ||
|
|
768210ef86 | ||
|
|
ce9d598683 | ||
|
|
2d75514aa1 | ||
|
|
669831c533 | ||
|
|
eb3f3ba441 | ||
|
|
46cf67e10f | ||
|
|
4e0b20b8e8 | ||
|
|
a6faad4b93 | ||
|
|
cb51ccae61 | ||
|
|
5a9d94aad0 | ||
|
|
69bdc7c0a2 | ||
|
|
2441c08da5 | ||
|
|
f9c244f006 | ||
|
|
9c5ff2b3a8 | ||
|
|
91bfb4d723 | ||
|
|
8d69418613 | ||
|
|
2d798053b3 | ||
|
|
e48385c70e | ||
|
|
4ed529bbfb | ||
|
|
ca5350cc96 | ||
|
|
9046245cdb | ||
|
|
991a512e85 | ||
|
|
6c008e3879 | ||
|
|
d16fbc3b13 | ||
|
|
a0354e30c7 | ||
|
|
0661bb0ea0 | ||
|
|
e72fe04525 | ||
|
|
59cec3d6a9 | ||
|
|
829f4ceaa4 | ||
|
|
b615e35e49 | ||
|
|
cda282ac6b | ||
|
|
f2d44a2fd1 | ||
|
|
52f3ba012b | ||
|
|
d43e6a19bf | ||
|
|
1f628e0d40 | ||
|
|
9922a495eb | ||
|
|
ae7549a00d | ||
|
|
a5e870035e | ||
|
|
38f1ab3075 | ||
|
|
c05248a1ab | ||
|
|
efacae517a | ||
|
|
70b71a44d3 | ||
|
|
0ac220341e | ||
|
|
fc8fd2c8eb | ||
|
|
f575eaafb6 | ||
|
|
2f79134023 | ||
|
|
c59e6268b3 | ||
|
|
3344e01a9c | ||
|
|
0a42031220 | ||
|
|
686e48f273 | ||
|
|
f16830adfe | ||
|
|
be8d09b777 | ||
|
|
5ed2b6d9c8 | ||
|
|
ece3ebc6e9 | ||
|
|
1afd8dc934 | ||
|
|
a239060798 | ||
|
|
05032d6c78 | ||
|
|
9dc35a603e | ||
|
|
43448aa482 | ||
|
|
8887e98249 | ||
|
|
43e4dcd760 | ||
|
|
94d4be7555 | ||
|
|
8210e2810f | ||
|
|
05be6876f3 | ||
|
|
38f8d88cdf | ||
|
|
3b0def17d7 | ||
|
|
57b0fa0b0e | ||
|
|
9611eb9c73 | ||
|
|
a2bbb6555b | ||
|
|
ddd25208d8 | ||
|
|
fe8db5831b | ||
|
|
0c29ade399 | ||
|
|
f8b2840d89 | ||
|
|
52a18c3c35 | ||
|
|
ace9cdcd68 | ||
|
|
7098c2266e | ||
|
|
58d3489c33 | ||
|
|
396d20f31e | ||
|
|
c8b866412a | ||
|
|
63b163e382 | ||
|
|
e1acc642ca | ||
|
|
e3f4cc68d2 | ||
|
|
983aff62f2 | ||
|
|
26e1ffdbdd | ||
|
|
2002519f30 | ||
|
|
c8f3e71d2c | ||
|
|
bd8f21e17a | ||
|
|
0981b1854b | ||
|
|
94ab823b50 | ||
|
|
b378f41c07 | ||
|
|
6cce077441 | ||
|
|
361e522c5e | ||
|
|
08161c892e | ||
|
|
39e4fdf40c | ||
|
|
11011f6b68 | ||
|
|
7eb2e08f03 | ||
|
|
7d422bea51 | ||
|
|
3367ec624c | ||
|
|
091db8e6aa | ||
|
|
5d0f2c5c60 | ||
|
|
71b9e87786 | ||
|
|
ae0b3572f8 | ||
|
|
217a09ebd6 | ||
|
|
ca61a75997 | ||
|
|
3310459b2c | ||
|
|
b000ca46c9 | ||
|
|
36bdf6db2b | ||
|
|
5f20a79c0a | ||
|
|
67eee7bb45 | ||
|
|
4b54c07a42 | ||
|
|
c907e158eb | ||
|
|
7a7d41803c | ||
|
|
b9604a6d02 | ||
|
|
794b612b8b | ||
|
|
8a965daa39 | ||
|
|
c0705e72e3 | ||
|
|
d755fd37bd | ||
|
|
14f41b886b | ||
|
|
ed8e6b479f | ||
|
|
f89b7ea7c4 | ||
|
|
bfb668ef3c | ||
|
|
c42e922d36 | ||
|
|
328926f924 | ||
|
|
cd6208674b | ||
|
|
568b2e4928 | ||
|
|
d850f6f60a | ||
|
|
fd7e32bbbd | ||
|
|
d84c403672 | ||
|
|
4cb322c72b | ||
|
|
56b98b027b | ||
|
|
e3b4d43c76 | ||
|
|
2b2e7d70ee | ||
|
|
ab0aefb6cb | ||
|
|
ac47c06e87 | ||
|
|
feb9c27ba7 | ||
|
|
c79ef68ce6 | ||
|
|
8c986bf9af | ||
|
|
a983e490d0 | ||
|
|
9802f04c5b | ||
|
|
5f1ff695f3 | ||
|
|
42a89ad311 | ||
|
|
6b0a6eb4a3 | ||
|
|
3b10ba7263 | ||
|
|
b3f804374f | ||
|
|
f1d0e0d70d | ||
|
|
431041b00e | ||
|
|
ff122acca0 | ||
|
|
0a58c4bbcc | ||
|
|
d55647998f | ||
|
|
0f3cc9c0d5 | ||
|
|
dce39288f4 | ||
|
|
74a516f3b6 | ||
|
|
627073a95c | ||
|
|
88af2516b8 | ||
|
|
6df37b148b | ||
|
|
88610db1c3 | ||
|
|
13dcd09ccd | ||
|
|
de298991a9 | ||
|
|
168b1d593c | ||
|
|
1a3643dc3c | ||
|
|
dcf178a010 | ||
|
|
4676e3ded5 | ||
|
|
1ad9438614 | ||
|
|
f2d87651d5 | ||
|
|
31f7814c0e | ||
|
|
ba57c00141 | ||
|
|
912c93b1ac | ||
|
|
db8d45c9e0 | ||
|
|
2b6aae4c0b | ||
|
|
df585a32e4 | ||
|
|
d36f58b642 | ||
|
|
337b8e9115 | ||
|
|
fa11f7ac82 | ||
|
|
91456fe216 | ||
|
|
b2c9255a53 | ||
|
|
16a6e7b0e7 | ||
|
|
fe5825c44a | ||
|
|
57b8fe3318 | ||
|
|
eb6fa2e982 | ||
|
|
0636bb4805 | ||
|
|
e83e4420fd | ||
|
|
d51cdbb4a8 | ||
|
|
80e42d12f3 | ||
|
|
958de2badf | ||
|
|
5efe66b481 | ||
|
|
df69e93cca | ||
|
|
4bd14e340c | ||
|
|
d5f8bc85f2 | ||
|
|
8d349a874b | ||
|
|
a1cc4c9263 | ||
|
|
834073fae1 | ||
|
|
2d5aa403a2 | ||
|
|
74fcd9e1db | ||
|
|
e90a3f701a | ||
|
|
0a54151b9a | ||
|
|
f310eb9855 | ||
|
|
aa02a19793 | ||
|
|
083b7762b1 | ||
|
|
b45257b284 | ||
|
|
d6051d6bcb | ||
|
|
f8e7d12252 | ||
|
|
8a74ce1d4b | ||
|
|
6715465dda | ||
|
|
aaca037e02 | ||
|
|
d1552e67d1 | ||
|
|
926967111a | ||
|
|
0b27a01013 | ||
|
|
eb4aebf637 | ||
|
|
91eb4b1c0d | ||
|
|
8614798362 | ||
|
|
405054034b | ||
|
|
d8766e1bdb | ||
|
|
6b11d284da | ||
|
|
12b41f5abb | ||
|
|
398c8b0eb6 | ||
|
|
357f68b3d2 | ||
|
|
5d74b0cd84 | ||
|
|
d72671d2c9 | ||
|
|
8f17fa82a2 | ||
|
|
111957b6e6 | ||
|
|
aa3292f988 | ||
|
|
cc0a72cecc | ||
|
|
fae8b57456 | ||
|
|
f2c3beff13 | ||
|
|
03790194ac | ||
|
|
2b5aebbc1b | ||
|
|
6183f38d8a | ||
|
|
29567c5392 | ||
|
|
43f9b8cf68 | ||
|
|
783070c74d | ||
|
|
59d6df5dce | ||
|
|
5fc93f61a2 | ||
|
|
abee8aa7c9 | ||
|
|
18d5194c38 | ||
|
|
4f794d299a | ||
|
|
623499b13b | ||
|
|
d48cb6b1f3 | ||
|
|
945ff8fc44 | ||
|
|
3a823c786e | ||
|
|
68a474644c | ||
|
|
c8b2ec9cdb | ||
|
|
67d4fdd175 | ||
|
|
0f40b38abe | ||
|
|
714350d40e | ||
|
|
d6ac0db0e6 | ||
|
|
ddae51cefc | ||
|
|
eb9f7c6cb5 | ||
|
|
a22f46087f | ||
|
|
2eedb361a0 | ||
|
|
d893964947 | ||
|
|
13838f66a9 | ||
|
|
6dccdae9b4 | ||
|
|
3f31fa9810 | ||
|
|
a17025bd3a | ||
|
|
f08ce0ee1a | ||
|
|
60634349ae | ||
|
|
14ce39116b | ||
|
|
edb6be1d51 | ||
|
|
8e2f79aea1 | ||
|
|
d6c4a9d2f6 | ||
|
|
d16a9f9d46 | ||
|
|
b817c7ac27 | ||
|
|
7a48949461 | ||
|
|
f1bad07c67 | ||
|
|
d2469f6313 | ||
|
|
f0ff8f7370 | ||
|
|
37cf3d6b1a | ||
|
|
8bda898480 | ||
|
|
4ca26c9104 | ||
|
|
265b95fc9e | ||
|
|
44619725c6 | ||
|
|
41ec908f37 | ||
|
|
b5ea5b21fd | ||
|
|
85763fa409 | ||
|
|
803c3cad7a | ||
|
|
2323008079 | ||
|
|
dcdcc5c557 | ||
|
|
a20d20dfce | ||
|
|
8a72826050 | ||
|
|
5c4aa457fa | ||
|
|
989b4d5695 | ||
|
|
0e89b8ac3f | ||
|
|
efebf795e4 | ||
|
|
fbdcd541e1 | ||
|
|
8e7d8370a1 | ||
|
|
73c593cc4e | ||
|
|
5b627350d8 | ||
|
|
d634c76040 | ||
|
|
55f7855cf8 | ||
|
|
f157ab9453 | ||
|
|
49c80cd76b | ||
|
|
cad15986bf | ||
|
|
9a0158da3a | ||
|
|
deea38c704 | ||
|
|
bc4fb455a3 | ||
|
|
411317194c | ||
|
|
42e9fac6f0 | ||
|
|
82d618cc03 | ||
|
|
9ebefdb1e8 | ||
|
|
5943158b3b | ||
|
|
e8fe386c1b | ||
|
|
5766833bd2 | ||
|
|
56f05e09ca | ||
|
|
b4ba209a8b | ||
|
|
75e2ed71be | ||
|
|
1e0f0cc5c7 | ||
|
|
e3626c2f9e | ||
|
|
6a13526c87 | ||
|
|
5f1b71520f | ||
|
|
bf687802d0 | ||
|
|
9b127470e2 | ||
|
|
7cfb1c6ee7 | ||
|
|
bcd96c08cf | ||
|
|
26f5516398 | ||
|
|
67eb359f15 | ||
|
|
60163a6026 | ||
|
|
cebbcd3c6c | ||
|
|
03d1e9a881 | ||
|
|
6fa4efaa5f | ||
|
|
52ca8f55a2 | ||
|
|
8b1c964082 | ||
|
|
171ab3a1ec | ||
|
|
1b3e97512b | ||
|
|
92a863b078 | ||
|
|
e693353d49 | ||
|
|
025487a5eb | ||
|
|
e19c0090de | ||
|
|
c6568f57fe | ||
|
|
d5d9f8024c | ||
|
|
292aad0d59 | ||
|
|
39c0ae8cf7 | ||
|
|
5ac386d56d | ||
|
|
a3ff882847 | ||
|
|
2cd6682cdf | ||
|
|
a3f17ea4d3 | ||
|
|
1b8c5e66bc | ||
|
|
0777ecd818 | ||
|
|
e309d7e049 | ||
|
|
61bb8c07c4 | ||
|
|
a3a875bc28 | ||
|
|
4e72e955ed | ||
|
|
24483ade28 | ||
|
|
38fee74250 | ||
|
|
f34da5afe5 | ||
|
|
08c26a461e | ||
|
|
7f8f2d9dda | ||
|
|
0a7704c06c | ||
|
|
a45d86c568 | ||
|
|
99e7ebee99 | ||
|
|
ec7ac4a036 | ||
|
|
9e5059fb38 | ||
|
|
9af896898e | ||
|
|
1259052b68 | ||
|
|
80976b4141 | ||
|
|
af1f14f293 | ||
|
|
cd175d50c1 | ||
|
|
17a1026696 | ||
|
|
ce4c09c373 | ||
|
|
105aca42a6 | ||
|
|
a54190adfc | ||
|
|
eda3eaa9a6 | ||
|
|
47366b3cf0 | ||
|
|
6e2c591822 | ||
|
|
5740ecd49c | ||
|
|
4c6d63bde8 | ||
|
|
a31e2295ee | ||
|
|
c0ff9a8502 | ||
|
|
6bcf8b1b15 | ||
|
|
8a836faa25 | ||
|
|
5a2028786a | ||
|
|
8a0f642d95 | ||
|
|
59a2180f2c | ||
|
|
2519edc6a7 | ||
|
|
42560d6603 |
273
.eslintrc.js
@@ -14,64 +14,97 @@ module.exports = {
|
||||
window: true,
|
||||
_: true
|
||||
},
|
||||
plugins: ['vue', 'spellcheck'],
|
||||
// add your custom rules here
|
||||
// it is base on https://github.com/vuejs/eslint-config-vue
|
||||
rules: {
|
||||
'vue/max-attributes-per-line': [2, {
|
||||
'singleline': 10,
|
||||
'multiline': {
|
||||
'max': 1,
|
||||
'allowFirstLine': false
|
||||
'vue/max-attributes-per-line': [
|
||||
2,
|
||||
{
|
||||
singleline: 10,
|
||||
multiline: {
|
||||
max: 1,
|
||||
allowFirstLine: false
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/name-property-casing': ['error', 'PascalCase'],
|
||||
'vue/no-v-html': 'off',
|
||||
'accessor-pairs': 2,
|
||||
'arrow-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'arrow-spacing': [
|
||||
2,
|
||||
{
|
||||
before: true,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
'block-spacing': [2, 'always'],
|
||||
'brace-style': [2, '1tbs', {
|
||||
'allowSingleLine': true
|
||||
}],
|
||||
'camelcase': [0, {
|
||||
'properties': 'always'
|
||||
}],
|
||||
'brace-style': [
|
||||
2,
|
||||
'1tbs',
|
||||
{
|
||||
allowSingleLine: true
|
||||
}
|
||||
],
|
||||
camelcase: [
|
||||
0,
|
||||
{
|
||||
properties: 'always'
|
||||
}
|
||||
],
|
||||
'comma-dangle': [2, 'never'],
|
||||
'comma-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
'comma-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
'comma-style': [2, 'last'],
|
||||
'constructor-super': 2,
|
||||
'curly': [2, 'multi-line'],
|
||||
curly: [2, 'multi-line'],
|
||||
'dot-location': [2, 'property'],
|
||||
'eol-last': 2,
|
||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
||||
'generator-star-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'generator-star-spacing': [
|
||||
2,
|
||||
{
|
||||
before: true,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
'handle-callback-err': [2, '^(err|error)$'],
|
||||
'indent': [2, 2, {
|
||||
'SwitchCase': 1
|
||||
}],
|
||||
indent: [
|
||||
2,
|
||||
2,
|
||||
{
|
||||
SwitchCase: 1
|
||||
}
|
||||
],
|
||||
'jsx-quotes': [2, 'prefer-single'],
|
||||
'key-spacing': [2, {
|
||||
'beforeColon': false,
|
||||
'afterColon': true
|
||||
}],
|
||||
'keyword-spacing': [2, {
|
||||
'before': true,
|
||||
'after': true
|
||||
}],
|
||||
'new-cap': [2, {
|
||||
'newIsCap': true,
|
||||
'capIsNew': false
|
||||
}],
|
||||
'key-spacing': [
|
||||
2,
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true
|
||||
}
|
||||
],
|
||||
'keyword-spacing': [
|
||||
2,
|
||||
{
|
||||
before: true,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
'new-cap': [
|
||||
2,
|
||||
{
|
||||
newIsCap: true,
|
||||
capIsNew: false
|
||||
}
|
||||
],
|
||||
'new-parens': 2,
|
||||
'no-array-constructor': 2,
|
||||
'no-caller': 2,
|
||||
@@ -102,17 +135,23 @@ module.exports = {
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-label-var': 2,
|
||||
'no-labels': [2, {
|
||||
'allowLoop': false,
|
||||
'allowSwitch': false
|
||||
}],
|
||||
'no-labels': [
|
||||
2,
|
||||
{
|
||||
allowLoop: false,
|
||||
allowSwitch: false
|
||||
}
|
||||
],
|
||||
'no-lone-blocks': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [2, {
|
||||
'max': 1
|
||||
}],
|
||||
'no-multiple-empty-lines': [
|
||||
2,
|
||||
{
|
||||
max: 1
|
||||
}
|
||||
],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new-object': 2,
|
||||
@@ -140,62 +179,126 @@ module.exports = {
|
||||
'no-undef-init': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unneeded-ternary': [2, {
|
||||
'defaultAssignment': false
|
||||
}],
|
||||
'no-unneeded-ternary': [
|
||||
2,
|
||||
{
|
||||
defaultAssignment: false
|
||||
}
|
||||
],
|
||||
'no-unreachable': 2,
|
||||
'no-unsafe-finally': 2,
|
||||
'no-unused-vars': [2, {
|
||||
'vars': 'all',
|
||||
'args': 'none'
|
||||
}],
|
||||
'no-unused-vars': [
|
||||
2,
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none'
|
||||
}
|
||||
],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-constructor': 2,
|
||||
'no-useless-escape': 0,
|
||||
'no-whitespace-before-property': 2,
|
||||
'no-with': 2,
|
||||
'one-var': [2, {
|
||||
'initialized': 'never'
|
||||
}],
|
||||
'operator-linebreak': [2, 'after', {
|
||||
'overrides': {
|
||||
'?': 'before',
|
||||
':': 'before'
|
||||
'one-var': [
|
||||
2,
|
||||
{
|
||||
initialized: 'never'
|
||||
}
|
||||
}],
|
||||
],
|
||||
'operator-linebreak': [
|
||||
2,
|
||||
'after',
|
||||
{
|
||||
overrides: {
|
||||
'?': 'before',
|
||||
':': 'before'
|
||||
}
|
||||
}
|
||||
],
|
||||
'padded-blocks': [2, 'never'],
|
||||
'quotes': [2, 'single', {
|
||||
'avoidEscape': true,
|
||||
'allowTemplateLiterals': true
|
||||
}],
|
||||
'semi': [2, 'never'],
|
||||
'semi-spacing': [2, {
|
||||
'before': false,
|
||||
'after': true
|
||||
}],
|
||||
quotes: [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true
|
||||
}
|
||||
],
|
||||
semi: [2, 'never'],
|
||||
'semi-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-before-function-paren': [
|
||||
2,
|
||||
{ anonymous: 'never', named: 'never', asyncArrow: 'always' }
|
||||
],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [2, {
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}],
|
||||
'spaced-comment': [2, 'always', {
|
||||
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
|
||||
}],
|
||||
'space-unary-ops': [
|
||||
2,
|
||||
{
|
||||
words: true,
|
||||
nonwords: false
|
||||
}
|
||||
],
|
||||
'object-curly-spacing': [2, 'always'],
|
||||
'spaced-comment': [
|
||||
2,
|
||||
'always',
|
||||
{
|
||||
markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
|
||||
}
|
||||
],
|
||||
'template-curly-spacing': [2, 'never'],
|
||||
'use-isnan': 2,
|
||||
'valid-typeof': 2,
|
||||
'wrap-iife': [2, 'any'],
|
||||
'yield-star-spacing': [2, 'both'],
|
||||
'yoda': [2, 'never'],
|
||||
yoda: [2, 'never'],
|
||||
'prefer-const': 2,
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'object-curly-spacing': [2, 'always', {
|
||||
objectsInObjects: false
|
||||
}],
|
||||
'array-bracket-spacing': [2, 'never']
|
||||
'array-bracket-spacing': [2, 'never'],
|
||||
'spellcheck/spell-checker': [
|
||||
'warn',
|
||||
{
|
||||
comments: true,
|
||||
strings: false,
|
||||
identifiers: false,
|
||||
lang: 'en_US',
|
||||
skipWords: [
|
||||
'echarts',
|
||||
'resize',
|
||||
'vue',
|
||||
'eslint',
|
||||
'babel',
|
||||
'jsx',
|
||||
'scss',
|
||||
'v-deep',
|
||||
'calc',
|
||||
'vw',
|
||||
'vh',
|
||||
'px',
|
||||
'rgba',
|
||||
'rgb',
|
||||
'var',
|
||||
'lang',
|
||||
'scoped',
|
||||
'pdf',
|
||||
'rbac'
|
||||
],
|
||||
skipIfMatch: [
|
||||
'http://[^s]*',
|
||||
'^[-\\w]+/[-\\w\\.]+$',
|
||||
String.raw`^\/api\/[a-z0-9\/._-]+$`,
|
||||
],
|
||||
minLength: 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
57
.prettierignore
Normal file
@@ -0,0 +1,57 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
lina/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Generated files
|
||||
*.min.js
|
||||
*.min.css
|
||||
|
||||
# Package files
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
|
||||
# Lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Build outputs
|
||||
*.map
|
||||
|
||||
# Config files that shouldn't be formatted
|
||||
.eslintrc.js
|
||||
babel.config.js
|
||||
jest.config.js
|
||||
vue.config.js
|
||||
postcss.config.js
|
||||
|
||||
# Theme files
|
||||
src/styles/fonts/
|
||||
public/fonts/
|
||||
lina/fonts/
|
||||
|
||||
# Assets
|
||||
src/assets/
|
||||
public/
|
||||
|
||||
# Mock data
|
||||
mock/
|
||||
|
||||
# Test files
|
||||
tests/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
11
.prettierrc.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
trailingComma: 'none',
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
bracketSpacing: true,
|
||||
arrowParens: 'avoid',
|
||||
endOfLine: 'auto'
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/lina-base:20240723_084702 AS stage-build
|
||||
FROM jumpserver/lina-base:20251204_081759 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
76
PRETTIER.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Prettier 配置说明
|
||||
|
||||
本项目已配置 Prettier 代码格式化工具,**仅在保存时自动格式化**,不进行批量格式化,以保持现有代码风格。
|
||||
|
||||
## 配置文件
|
||||
|
||||
- `.prettierrc` - Prettier 配置文件
|
||||
- `.prettierignore` - 忽略格式化的文件列表
|
||||
- `.vscode/settings.json` - VSCode 编辑器配置(保存时自动格式化)
|
||||
- `.vscode/extensions.json` - 推荐的 VSCode 扩展
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 安装依赖
|
||||
项目已安装以下依赖:
|
||||
- `prettier@^2.8.8` - Prettier 核心
|
||||
- `eslint-plugin-prettier@^3.1.4` - ESLint 与 Prettier 集成
|
||||
- `eslint-config-prettier@^6.15.0` - 禁用与 Prettier 冲突的 ESLint 规则
|
||||
|
||||
### 2. 命令行使用
|
||||
|
||||
```bash
|
||||
# ESLint 检查和修复
|
||||
npm run fix
|
||||
```
|
||||
|
||||
**注意**:本项目配置为仅在保存时自动格式化,不提供批量格式化命令。
|
||||
|
||||
### 3. VSCode 编辑器配置
|
||||
|
||||
确保安装了推荐的扩展:
|
||||
- Prettier - Code formatter (esbenp.prettier-vscode)
|
||||
- ESLint (dbaeumer.vscode-eslint)
|
||||
- Vetur (octref.vetur)
|
||||
|
||||
配置已设置为保存时自动格式化。
|
||||
|
||||
### 4. Git 提交钩子
|
||||
|
||||
项目使用 `husky` 和 `lint-staged` 在提交时进行代码检查:
|
||||
- 提交时运行 ESLint 检查和修复
|
||||
- 不进行批量格式化,保持原有代码风格
|
||||
|
||||
## Prettier 配置说明
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": false, // 不使用分号
|
||||
"singleQuote": true, // 使用单引号
|
||||
"tabWidth": 0, // 不使用缩进
|
||||
"useTabs": false, // 使用空格而不是制表符
|
||||
"trailingComma": "none", // 不使用尾随逗号
|
||||
"printWidth": 100, // 行宽 100 字符
|
||||
"bracketSpacing": true, // 对象括号内有空格
|
||||
"arrowParens": "avoid", // 箭头函数单参数时不使用括号
|
||||
"endOfLine": "lf", // 使用 LF 换行符
|
||||
"vueIndentScriptAndStyle": false // Vue 文件中 script 和 style 标签不缩进
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何临时禁用格式化?
|
||||
A: 使用注释:
|
||||
```javascript
|
||||
// prettier-ignore
|
||||
const uglyCode = {
|
||||
a:1,b:2
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 如何添加文件到忽略列表?
|
||||
A: 编辑 `.prettierignore` 文件,添加文件或目录路径。
|
||||
|
||||
### Q: VSCode 保存时没有自动格式化?
|
||||
A: 检查是否安装了 Prettier 扩展,并确认 `.vscode/settings.json` 配置正确。
|
||||
@@ -1,13 +1,25 @@
|
||||
import Mock from 'mockjs'
|
||||
import { param2Obj } from '../src/utils'
|
||||
|
||||
import user from './user'
|
||||
import table from './table'
|
||||
|
||||
const mocks = [
|
||||
...user,
|
||||
...table
|
||||
]
|
||||
export function param2Obj(url) {
|
||||
const search = url.split('?')[1]
|
||||
if (!search) {
|
||||
return {}
|
||||
}
|
||||
return JSON.parse(
|
||||
'{"' +
|
||||
decodeURIComponent(search)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/&/g, '","')
|
||||
.replace(/=/g, '":"')
|
||||
.replace(/\+/g, ' ') +
|
||||
'"}'
|
||||
)
|
||||
}
|
||||
|
||||
const mocks = [...user, ...table]
|
||||
|
||||
// for front mock
|
||||
// please use it cautiously, it will redefine XMLHttpRequest,
|
||||
|
||||
46
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "lina",
|
||||
"version": "v4.0.0",
|
||||
"description": "JumpServer Web UI",
|
||||
"author": "JumpServer Team <support@fit2cloud.com>",
|
||||
"author": "JumpServer Team <support@lxware.hk>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
|
||||
@@ -26,19 +26,22 @@
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||
"@fontsource/open-sans": "^5.0.24",
|
||||
"@kangc/v-md-editor": "^1.7.12",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@ztree/ztree_v3": "3.5.44",
|
||||
"axios": "0.28.0",
|
||||
"axios-retry": "^3.1.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
"cache-loader": "^4.1.0",
|
||||
"caniuse-lite": "^1.0.30001642",
|
||||
"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",
|
||||
"dompurify": "^3.1.6",
|
||||
"dompurify": "^3.2.4",
|
||||
"echarts": "4.7.0",
|
||||
"element-ui": "2.15.14",
|
||||
"element-ui": "https://github.com/jumpserver-dev/element/releases/download/v2.15.15/jumpserver-element-ui-2.15.15.tgz",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"install": "^0.13.0",
|
||||
@@ -48,17 +51,6 @@
|
||||
"less": "^3.10.3",
|
||||
"less-loader": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.frompairs": "^4.0.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.has": "^4.5.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"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",
|
||||
@@ -67,20 +59,23 @@
|
||||
"npm": "^7.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sortablejs": "^1.15.6",
|
||||
"uuid": "8.3.2",
|
||||
"v-sanitize": "^0.0.13",
|
||||
"vue": "2.6.10",
|
||||
"vue": "2.7.16",
|
||||
"vue-codemirror": "4.0.6",
|
||||
"vue-cookie": "^1.1.4",
|
||||
"vue-echarts": "^5.0.0-beta.0",
|
||||
"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",
|
||||
"vuejs-logger": "^1.5.4",
|
||||
"vuex": "3.1.0",
|
||||
"watermark-js-plus": "^1.5.8",
|
||||
"xss": "^1.0.14",
|
||||
"xterm": "^4.5.0",
|
||||
"xterm-addon-fit": "^0.3.0",
|
||||
@@ -94,17 +89,18 @@
|
||||
"@vue/cli-plugin-unit-jest": "3.6.3",
|
||||
"@vue/cli-service": "3.6.0",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"autoprefixer": "^9.5.1",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "23.6.0",
|
||||
"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-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-spellcheck": "^0.0.20",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
"eslint-plugin-vue-i18n": "^0.3.0",
|
||||
"github-markdown-css": "^5.1.0",
|
||||
@@ -113,6 +109,8 @@
|
||||
"less-loader": "^5.0.0",
|
||||
"lint-staged": "^10.1.2",
|
||||
"mockjs": "1.0.1-beta3",
|
||||
"prettier": "^3.6.2",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"runjs": "^4.3.2",
|
||||
"sass": "~1.32.6",
|
||||
"sass-loader": "^7.1.0",
|
||||
@@ -121,12 +119,13 @@
|
||||
"serve-static": "^1.16.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.4",
|
||||
"svgo": "1.2.2",
|
||||
"vue-i18n-extract": "^1.1.1",
|
||||
"vue-template-compiler": "2.6.10"
|
||||
"vue-template-compiler": "2.7.16",
|
||||
"webpack": "^4.28.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9",
|
||||
"node": ">=12",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
@@ -143,5 +142,6 @@
|
||||
"src/**/*.{js,vue}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
78
src/App.vue
@@ -5,14 +5,90 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { Watermark } from 'watermark-js-plus'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
watermark: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isRouterAlive: state => state.common.isRouterAlive
|
||||
}),
|
||||
...mapGetters({
|
||||
currentUser: 'currentUser',
|
||||
publicSettings: 'publicSettings'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
currentUser: {
|
||||
handler(newVal) {
|
||||
this.createWatermark()
|
||||
}
|
||||
},
|
||||
'publicSettings.SECURITY_WATERMARK_ENABLED': {
|
||||
handler(newVal) {
|
||||
if (!newVal) {
|
||||
return setTimeout(() => {
|
||||
this.watermark?.destroy()
|
||||
this.watermark = null
|
||||
})
|
||||
}
|
||||
|
||||
this.createWatermark()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWaterMarkFields() {
|
||||
const user = this.currentUser
|
||||
const userId = user?.id || ''
|
||||
const name = user?.name || ''
|
||||
const userName = user?.username || ''
|
||||
const currentTime = this.$moment(new Date()).format('YYYY-MM-DD HH:mm:ss')
|
||||
return { userId, name, userName, currentTime }
|
||||
},
|
||||
getWaterMarkContent() {
|
||||
const fields = this.getWaterMarkFields()
|
||||
const template = this.publicSettings.SECURITY_WATERMARK_CONSOLE_CONTENT || ''
|
||||
|
||||
// 找出模板中所有的变量占位符 ${xxx}
|
||||
const placeholders = template.match(/\${([^}]+)}/g) || []
|
||||
const allVariables = {}
|
||||
|
||||
// 为模板中的每个变量准备值
|
||||
placeholders.forEach(placeholder => {
|
||||
const varName = placeholder.slice(2, -1) // 提取变量名,去掉 ${ 和 }
|
||||
allVariables[varName] = fields[varName] !== undefined ? fields[varName] : 'N/A'
|
||||
})
|
||||
|
||||
// 合并用户现有的字段和模板中可能缺失的字段
|
||||
const safeFields = { ...fields, ...allVariables }
|
||||
|
||||
// 安全解析模板
|
||||
return new Function(...Object.keys(safeFields), `return \`${template}\`;`)(...Object.values(safeFields))
|
||||
},
|
||||
|
||||
createWatermark() {
|
||||
if (this.currentUser?.username && this.publicSettings?.SECURITY_WATERMARK_ENABLED) {
|
||||
this.watermark = new Watermark({
|
||||
content: this.getWaterMarkContent(),
|
||||
width: this.publicSettings?.SECURITY_WATERMARK_WIDTH,
|
||||
height: this.publicSettings?.SECURITY_WATERMARK_HEIGHT,
|
||||
rotate: this.publicSettings?.SECURITY_WATERMARK_ROTATE,
|
||||
fontWeight: 'normal',
|
||||
fontSize: this.publicSettings?.SECURITY_WATERMARK_FONT_SIZE + 'px',
|
||||
fontColor: this.publicSettings?.SECURITY_WATERMARK_COLOR,
|
||||
contentType: 'multi-line-text',
|
||||
lineHeight: this.publicSettings?.SECURITY_WATERMARK_FONT_SIZE
|
||||
})
|
||||
this.watermark.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,9 +16,9 @@ export function getSystemUserList(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getDomainList(data) {
|
||||
export function getZoneList(data) {
|
||||
return request({
|
||||
url: '/api/v1/assets/domains/',
|
||||
url: '/api/v1/assets/zones/',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
|
||||
@@ -61,13 +61,14 @@ export function stopJob(form) {
|
||||
})
|
||||
}
|
||||
|
||||
export function JobUploadFile(form) {
|
||||
export function JobUploadFile(form, config = {}) {
|
||||
return request({
|
||||
url: '/api/v1/ops/jobs/upload/',
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60 * 60 * 1000,
|
||||
data: form
|
||||
data: form,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ export function toggleLockSession(data) {
|
||||
|
||||
export function getAllCommandStorage() {
|
||||
return request({
|
||||
url: `/api/v1/terminal/command-storages/`,
|
||||
url: '/api/v1/terminal/command-storages/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAllReplayStorage() {
|
||||
return request({
|
||||
url: `/api/v1/terminal/replay-storages/`,
|
||||
url: '/api/v1/terminal/replay-storages/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,11 +8,20 @@ export function login(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getProfile(token) {
|
||||
return request({
|
||||
export async function getProfile(token) {
|
||||
let profile = await request({
|
||||
url: '/api/v1/users/profile/',
|
||||
method: 'get'
|
||||
})
|
||||
const perms = await request({
|
||||
url: '/api/v1/users/profile/permissions/',
|
||||
method: 'get'
|
||||
})
|
||||
profile = {
|
||||
...profile,
|
||||
...perms
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
export function getUserList(data) {
|
||||
@@ -22,6 +31,7 @@ export function getUserList(data) {
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserGroupList(params) {
|
||||
return request({
|
||||
url: '/api/v1/users/groups/',
|
||||
@@ -29,6 +39,7 @@ export function getUserGroupList(params) {
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserGroupDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/users/groups/${id}/`,
|
||||
@@ -50,6 +61,7 @@ export function editUserGroup(data) {
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUserGroup(id, data) {
|
||||
return request({
|
||||
url: '/api/v1/users/groups/' + id + '/',
|
||||
|
||||
BIN
src/assets/img/auth/cas_logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/img/auth/dingtalk_logo.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/img/auth/feishu_logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/img/auth/lark_logo.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/assets/img/auth/ldap_logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/img/auth/oauth2_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/img/auth/oidc_logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/img/auth/passkey_logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/img/auth/radius_logo.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/img/auth/saml2_logo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/img/auth/slack_logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/img/auth/wecom_logo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
1
src/assets/img/cloud/proxmox.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1752631175762" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4421" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M93.866667 234.666667c-34.133333 12.8-51.2 25.6-51.2 34.133333 0 4.266667 51.2 64 115.2 136.533333 64 68.266667 115.2 128 115.2 128s-51.2 59.733333-115.2 128C93.866667 729.6 42.666667 793.6 42.666667 797.866667c4.266667 17.066667 59.733333 42.666667 98.133333 42.666666 64 0 81.066667-12.8 217.6-162.133333 68.266667-76.8 128-140.8 128-140.8 0-4.266667-55.466667-64-123.733333-140.8-85.333333-102.4-132.266667-145.066667-153.6-157.866667-29.866667-12.8-81.066667-17.066667-115.2-4.266666z m725.333333 4.266666c-21.333333 8.533333-68.266667 59.733333-153.6 153.6-68.266667 76.8-123.733333 140.8-123.733333 140.8 0 4.266667 55.466667 68.266667 128 140.8 136.533333 153.6 153.6 162.133333 217.6 162.133334 42.666667 0 98.133333-21.333333 98.133333-42.666667 0-4.266667-51.2-68.266667-115.2-136.533333-64-68.266667-115.2-128-115.2-128s51.2-59.733333 115.2-128c64-68.266667 115.2-128 115.2-136.533334-4.266667-17.066667-55.466667-38.4-98.133333-38.4-34.133333 0-46.933333 4.266667-68.266667 12.8z" fill="#E57000" p-id="4422"></path><path d="M238.933333 136.533333c-42.666667 21.333333-42.666667 25.6-4.266666 68.266667 221.866667 243.2 273.066667 302.933333 277.333333 302.933333 4.266667 0 311.466667-332.8 315.733333-341.333333 0-4.266667-8.533333-12.8-21.333333-21.333333-17.066667-12.8-34.133333-17.066667-68.266667-17.066667-64-4.266667-85.333333 8.533333-162.133333 93.866667-34.133333 38.4-64 72.533333-64 72.533333s-29.866667-29.866667-64-68.266667c-34.133333-38.4-72.533333-76.8-89.6-85.333333-25.6-17.066667-89.6-21.333333-119.466667-4.266667zM354.133333 725.333333c-85.333333 93.866667-153.6 170.666667-153.6 174.933334 0 4.266667 8.533333 12.8 21.333334 21.333333 17.066667 12.8 34.133333 17.066667 68.266666 17.066667 59.733333 4.266667 85.333333-8.533333 162.133334-98.133334 34.133333-38.4 64-68.266667 64-68.266666s29.866667 29.866667 64 68.266666c81.066667 89.6 98.133333 102.4 162.133333 98.133334 34.133333 0 51.2-4.266667 68.266667-17.066667 12.8-8.533333 21.333333-17.066667 21.333333-21.333333-4.266667-8.533333-311.466667-345.6-315.733333-341.333334-8.533333 0-76.8 76.8-162.133334 166.4z" p-id="4423"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
src/assets/img/cloud/smartx.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748326203303" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2853" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M10.24 605.184l839.168-481.28L1013.76 220.672v191.488L174.592 895.488 10.24 804.352z" fill="#0096FF" p-id="2854"></path><path d="M10.24 416.768V220.672l168.96-96.768 308.736 178.688-331.776 193.536zM541.184 717.312l331.264-195.072 141.312 88.064v194.048l-165.376 95.744z" fill="#25C764" p-id="2855"></path></svg>
|
||||
|
After Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/img/dream_bg.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/img/icons/windows_ad.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,25 +1,42 @@
|
||||
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']
|
||||
|
||||
function onPrivilegedUser(value, updateForm) {
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true, secret_reset: false, push_now: false })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
assets: {
|
||||
nodes: {
|
||||
component: Select2,
|
||||
label: vm.$t('Assets'),
|
||||
rules: [Required],
|
||||
label: vm.$t('Node'),
|
||||
el: {
|
||||
multiple: true,
|
||||
value: [],
|
||||
ajax: {
|
||||
url: '/api/v1/assets/assets/',
|
||||
url: '/api/v1/assets/nodes/',
|
||||
transformOption: (item) => {
|
||||
return { label: item.name + '(' + item.address + ')', value: item.id }
|
||||
return { label: item.full_value, value: item.id }
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return !vm.addTemplate
|
||||
}
|
||||
},
|
||||
assets: {
|
||||
component: AssetSelect,
|
||||
label: vm.$t('Asset'),
|
||||
el: {
|
||||
multiple: false
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset
|
||||
}
|
||||
@@ -31,7 +48,7 @@ export const accountFieldsMeta = (vm) => {
|
||||
get disabled() {
|
||||
return vm.isDisabled
|
||||
},
|
||||
multiple: false,
|
||||
multiple: vm.addTemplate,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
transformOption: (item) => {
|
||||
@@ -70,11 +87,8 @@ export const accountFieldsMeta = (vm) => {
|
||||
if (!vm.account?.name) {
|
||||
updateForm({ username: value })
|
||||
}
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
onPrivilegedUser(value, updateForm)
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
@@ -92,10 +106,7 @@ export const accountFieldsMeta = (vm) => {
|
||||
vm.usernameChanged = true
|
||||
},
|
||||
change: ([value], updateForm) => {
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
onPrivilegedUser(value, updateForm)
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
|
||||
export default {
|
||||
@@ -63,7 +63,7 @@ export default {
|
||||
encryptedFields: ['secret'],
|
||||
fields: [
|
||||
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
|
||||
[this.$t('Assets'), ['assets']],
|
||||
[this.$t('Asset'), ['nodes', 'assets']],
|
||||
[this.$t('Secret'), [
|
||||
'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script>
|
||||
import { GenericUpdateFormDialog } from '@/layout/components'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
|
||||
export default {
|
||||
name: 'AccountBulkUpdateDialog',
|
||||
|
||||
@@ -93,8 +93,8 @@ export default {
|
||||
iVisible = true
|
||||
data = formValue
|
||||
url = `/api/v1/accounts/accounts/bulk/`
|
||||
if (data.assets.length === 0) {
|
||||
this.$message.error(this.$tc('PleaseSelectAsset'))
|
||||
if ((!data.assets || data.assets.length === 0) && (!data.nodes || data.nodes.length === 0)) {
|
||||
this.$message.error(this.$tc('PleaseSelectAssetOrNode'))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,10 @@ export default {
|
||||
this.$emit('add', true)
|
||||
}
|
||||
}).catch(error => {
|
||||
if (error?.response?.data?.code === 'no_valid_assets') {
|
||||
this.$message.error(error?.response?.data?.detail)
|
||||
return
|
||||
}
|
||||
this.iVisible = true
|
||||
this.handleResult(null, error)
|
||||
})
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { accountOtherActions, accountQuickFilters, connectivityMeta } from './const'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import { accountOtherActions, accountQuickFilters, connectivityMeta, isDirectoryServiceAccount } from './const'
|
||||
import { openTaskPage } from '@/utils/jms/index'
|
||||
import {
|
||||
AccountConnectFormatter,
|
||||
ActionsFormatter,
|
||||
@@ -182,13 +182,21 @@ export default {
|
||||
},
|
||||
columnsMeta: {
|
||||
name: {
|
||||
width: '120px',
|
||||
minWidth: '60px',
|
||||
formatterArgs: {
|
||||
can: () => vm.$hasPerm('accounts.view_account'),
|
||||
getRoute: ({ row }) => ({
|
||||
name: 'AccountDetail',
|
||||
params: { id: row.id }
|
||||
}),
|
||||
getTitle: ({ row }) => {
|
||||
let title = row.name
|
||||
if (row.ds && this.asset && this.asset.id !== row.asset.id) {
|
||||
const dsID = row.ds.id.split('-')[0]
|
||||
title = `${row.name}@${dsID}`
|
||||
}
|
||||
return title
|
||||
},
|
||||
getDrawerTitle({ row }) {
|
||||
return `${row.username}@${row.asset.name}`
|
||||
}
|
||||
@@ -208,15 +216,19 @@ export default {
|
||||
width: '80px',
|
||||
formatter: AccountConnectFormatter,
|
||||
formatterArgs: {
|
||||
buttonIcon: 'fa fa-desktop',
|
||||
url: '/api/v1/assets/assets/{id}',
|
||||
can: () => this.currentUserIsSuperAdmin,
|
||||
connectUrlTemplate: (row) => `/luna/pam_connect/${row.id}/${row.username}/${row.asset.id}/${row.asset.name}/`,
|
||||
setMapItem: (id, protocol) => {
|
||||
this.$store.commit('table/SET_PROTOCOL_MAP_ITEM', {
|
||||
key: id,
|
||||
value: protocol
|
||||
})
|
||||
asset: this.asset,
|
||||
can: ({ row }) => {
|
||||
return this.currentUserIsSuperAdmin
|
||||
}
|
||||
}
|
||||
},
|
||||
ds: {
|
||||
width: '100px',
|
||||
formatter: (row) => {
|
||||
if (row.ds && row.ds['domain_name']) {
|
||||
return row.ds['domain_name']
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -229,12 +241,20 @@ export default {
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
minWidth: '100px',
|
||||
formatter: function(row) {
|
||||
return row.asset.name
|
||||
}
|
||||
},
|
||||
username: {
|
||||
width: '120px'
|
||||
minWidth: '60px',
|
||||
formatter: function(row) {
|
||||
if (row.ds && row.ds['domain_name']) {
|
||||
return `${row.username}@${row.ds['domain_name']}`
|
||||
} else {
|
||||
return row.username
|
||||
}
|
||||
}
|
||||
},
|
||||
secret_type: {
|
||||
formatter: function(row) {
|
||||
@@ -264,10 +284,15 @@ export default {
|
||||
formatter: ActionsFormatter,
|
||||
has: this.showActions,
|
||||
formatterArgs: {
|
||||
performDelete: ({ row }) => {
|
||||
const id = row.id
|
||||
const url = `/api/v1/accounts/accounts/${id}/`
|
||||
return this.$axios.delete(url)
|
||||
},
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: true, // can set function(row, value)
|
||||
hasClone: false,
|
||||
canDelete: () => vm.$hasPerm('accounts.delete_account'),
|
||||
canDelete: ({ row }) => vm.$hasPerm('accounts.delete_account') && !isDirectoryServiceAccount(row, this),
|
||||
moreActionsTitle: this.$t('More'),
|
||||
extraActions: accountOtherActions(this)
|
||||
}
|
||||
@@ -316,7 +341,7 @@ export default {
|
||||
can: () => {
|
||||
return vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: async() => {
|
||||
callback: async () => {
|
||||
await this.getAssetDetail()
|
||||
setTimeout(() => {
|
||||
vm.iAsset = this.asset
|
||||
@@ -337,7 +362,7 @@ export default {
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 &&
|
||||
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
|
||||
!this.$store.getters.currentOrgIsRoot
|
||||
!this.$store.getters.currentOrgIsRoot && vm.$hasPerm('accounts.verify_account')
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => {
|
||||
@@ -453,7 +478,7 @@ export default {
|
||||
this.$refs.ListTable.reloadTable()
|
||||
},
|
||||
async getAssetDetail() {
|
||||
const { query: { asset }} = this.$route
|
||||
const { query: { asset } } = this.$route
|
||||
if (asset) {
|
||||
this.iAsset = await this.$axios.get(`/api/v1/assets/assets/${asset}/`)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export default {
|
||||
prop: 'asset',
|
||||
label: this.$t('Asset')
|
||||
},
|
||||
{
|
||||
prop: 'account',
|
||||
label: this.$t('Account')
|
||||
},
|
||||
{
|
||||
prop: 'state',
|
||||
label: this.$t('Status'),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import { openTaskPage } from '@/utils/jms/index'
|
||||
|
||||
export default {
|
||||
name: 'RemoveAccount',
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
|
||||
export default {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import { openTaskPage } from '@/utils/jms/index'
|
||||
|
||||
export const connectivityMeta = {
|
||||
formatter: ChoicesFormatter,
|
||||
@@ -7,11 +7,23 @@ export const connectivityMeta = {
|
||||
faChoices: {
|
||||
'-': '',
|
||||
ok: 'fa-check-circle',
|
||||
err: 'fa-times-circle'
|
||||
err: 'fa-times-circle',
|
||||
auth_err: 'fa-times-circle',
|
||||
rdp_err: 'fa-times-circle',
|
||||
password_err: 'fa-times-circle',
|
||||
openssh_key_err: 'fa-times-circle',
|
||||
ntlm_err: 'fa-times-circle',
|
||||
create_temp_err: 'fa-times-circle'
|
||||
},
|
||||
classChoices: {
|
||||
ok: 'text-primary',
|
||||
err: 'text-danger'
|
||||
err: 'text-danger',
|
||||
auth_err: 'text-danger',
|
||||
rdp_err: 'text-danger',
|
||||
password_err: 'text-danger',
|
||||
openssh_key_err: 'text-danger',
|
||||
ntlm_err: 'text-danger',
|
||||
create_temp_err: 'text-danger'
|
||||
},
|
||||
getText({ cellValue }) {
|
||||
if (cellValue?.value === '-' || cellValue?.value === 'unknown') {
|
||||
@@ -24,164 +36,200 @@ export const connectivityMeta = {
|
||||
width: '130px'
|
||||
}
|
||||
|
||||
export const accountOtherActions = (vm) => [
|
||||
{
|
||||
name: 'View',
|
||||
title: vm.$t('View'),
|
||||
can: vm.$hasPerm('accounts.view_accountsecret'),
|
||||
type: 'primary',
|
||||
order: 1,
|
||||
callback: ({ row }) => {
|
||||
// debugger
|
||||
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
|
||||
vm.account = row
|
||||
vm.showViewSecretDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showViewSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: vm.$t('Edit'),
|
||||
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
vm.isUpdateAccount = true
|
||||
const data = {
|
||||
...vm.asset,
|
||||
...row.asset
|
||||
}
|
||||
vm.iAsset = data
|
||||
vm.account = row
|
||||
vm.addTemplate = false
|
||||
vm.showAddDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'UpdateSecret',
|
||||
title: vm.$t('EditSecret'),
|
||||
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
const data = {
|
||||
...vm.asset,
|
||||
...row.asset
|
||||
}
|
||||
vm.account = row
|
||||
vm.iAsset = data
|
||||
vm.showUpdateSecretDialog = false
|
||||
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
|
||||
setTimeout(() => {
|
||||
vm.showUpdateSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Clone',
|
||||
title: vm.$t('Duplicate'),
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
},
|
||||
can: vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
vm.account = {
|
||||
name: `${row.name} - ${vm.$t('Duplicate').toLowerCase()}`,
|
||||
username: `${row.username} - ${vm.$t('Duplicate').toLowerCase()}`,
|
||||
payload: 'pam_account_clone'
|
||||
}
|
||||
vm.iAsset = vm.asset
|
||||
export function isDirectoryServiceAccount(account, vm) {
|
||||
return vm.asset && vm.asset.id !== account.asset.id
|
||||
}
|
||||
|
||||
vm.showAddDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: vm.$t('VerifySecret'),
|
||||
divided: true,
|
||||
can: ({ row }) =>
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
vm.$hasPerm('accounts.verify_account') &&
|
||||
row.asset['auto_config'].ansible_enabled &&
|
||||
row.asset['auto_config'].ping_enabled,
|
||||
callback: ({ row }) => {
|
||||
vm.$axios.post(
|
||||
`/api/v1/accounts/accounts/tasks/`,
|
||||
{ action: 'verify', accounts: [row.id] }
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ClearSecret',
|
||||
title: vm.$t('ClearSecret'),
|
||||
can: vm.$hasPerm('accounts.change_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
vm.$axios.patch(
|
||||
`/api/v1/accounts/accounts/clear-secret/`,
|
||||
{ account_ids: [row.id] }
|
||||
).then(() => {
|
||||
vm.$message.success(vm.$tc('ClearSuccessMsg'))
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'SecretHistory',
|
||||
title: vm.$t('HistoryPassword'),
|
||||
can: () => vm.$hasPerm('accounts.view_accountsecret'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
vm.account = row
|
||||
vm.currentAccountColumn = row
|
||||
vm.showViewSecretDialog = false
|
||||
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
|
||||
setTimeout(() => {
|
||||
vm.showViewSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'CopyToOther',
|
||||
title: vm.$t('CopyToAsset'),
|
||||
type: 'primary',
|
||||
divided: true,
|
||||
can: () => vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
export const accountOtherActions = vm => {
|
||||
return [
|
||||
{
|
||||
name: 'View',
|
||||
title: vm.$t('View'),
|
||||
can: vm.$hasPerm('accounts.view_accountsecret'),
|
||||
type: 'primary',
|
||||
order: 1,
|
||||
callback: ({ row }) => {
|
||||
// debugger
|
||||
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
|
||||
vm.account = row
|
||||
vm.showViewSecretDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showViewSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.accountCreateUpdateTitle = vm.$t('CopyToOther')
|
||||
vm.$route.query.flag = 'copy'
|
||||
vm.iAsset = vm.asset
|
||||
vm.account = row
|
||||
vm.showAddDialog = true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MoveToOther',
|
||||
title: vm.$t('MoveToAsset'),
|
||||
type: 'primary',
|
||||
can: () => vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
{
|
||||
name: 'Update',
|
||||
title: vm.$t('Edit'),
|
||||
can: ({ row }) => {
|
||||
return (
|
||||
vm.$hasPerm('accounts.change_account') &&
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
!isDirectoryServiceAccount(row, vm)
|
||||
)
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.isUpdateAccount = true
|
||||
const data = {
|
||||
...vm.asset,
|
||||
...row.asset
|
||||
}
|
||||
vm.iAsset = data
|
||||
vm.account = row
|
||||
vm.addTemplate = false
|
||||
vm.showAddDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.accountCreateUpdateTitle = vm.$t('MoveToOther')
|
||||
vm.$route.query.flag = 'move'
|
||||
vm.iAsset = vm.asset
|
||||
vm.account = row
|
||||
vm.showAddDialog = true
|
||||
}
|
||||
}
|
||||
]
|
||||
{
|
||||
name: 'UpdateSecret',
|
||||
title: vm.$t('EditSecret'),
|
||||
can: ({ row }) => {
|
||||
return (
|
||||
vm.$hasPerm('accounts.change_account') &&
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
!isDirectoryServiceAccount(row, vm)
|
||||
)
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
const data = {
|
||||
...vm.asset,
|
||||
...row.asset
|
||||
}
|
||||
vm.account = row
|
||||
vm.iAsset = data
|
||||
vm.showUpdateSecretDialog = false
|
||||
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
|
||||
setTimeout(() => {
|
||||
vm.showUpdateSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Clone',
|
||||
title: vm.$t('Duplicate'),
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
},
|
||||
can: ({ row }) => {
|
||||
return (
|
||||
vm.$hasPerm('accounts.add_account') &&
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
!isDirectoryServiceAccount(row, vm)
|
||||
)
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.account = {
|
||||
name: `${row.name} - ${vm.$t('Duplicate').toLowerCase()}`,
|
||||
username: `${row.username} - ${vm.$t('Duplicate').toLowerCase()}`,
|
||||
payload: 'pam_account_clone'
|
||||
}
|
||||
vm.iAsset = vm.asset
|
||||
|
||||
export const accountQuickFilters = (vm) => [
|
||||
vm.showAddDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: vm.$t('VerifySecret'),
|
||||
divided: true,
|
||||
can: ({ row }) =>
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
vm.$hasPerm('accounts.verify_account') &&
|
||||
row.asset['auto_config'].ansible_enabled &&
|
||||
row.asset['auto_config'].ping_enabled,
|
||||
callback: ({ row }) => {
|
||||
vm.$axios
|
||||
.post(`/api/v1/accounts/accounts/tasks/`, { action: 'verify', accounts: [row.id] })
|
||||
.then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ClearSecret',
|
||||
title: vm.$t('ClearSecret'),
|
||||
can: ({ row }) => {
|
||||
return vm.$hasPerm('accounts.change_account') && !isDirectoryServiceAccount(row, vm)
|
||||
},
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
vm.$axios
|
||||
.patch(`/api/v1/accounts/accounts/clear-secret/`, { account_ids: [row.id] })
|
||||
.then(() => {
|
||||
vm.$message.success(vm.$tc('ClearSuccessMsg'))
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'SecretHistory',
|
||||
title: vm.$t('HistoryPassword'),
|
||||
can: () => vm.$hasPerm('accounts.view_accountsecret'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
vm.account = row
|
||||
vm.currentAccountColumn = row
|
||||
vm.showViewSecretDialog = false
|
||||
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
|
||||
setTimeout(() => {
|
||||
vm.showViewSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'CopyToOther',
|
||||
title: vm.$t('CopyToAsset'),
|
||||
type: 'primary',
|
||||
divided: true,
|
||||
can: ({ row }) => {
|
||||
return (
|
||||
vm.$hasPerm('accounts.add_account') &&
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
!isDirectoryServiceAccount(row, vm)
|
||||
)
|
||||
},
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.accountCreateUpdateTitle = vm.$t('CopyToOther')
|
||||
vm.$route.query.flag = 'copy'
|
||||
vm.iAsset = vm.asset
|
||||
vm.account = row
|
||||
vm.showAddDialog = true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MoveToOther',
|
||||
title: vm.$t('MoveToAsset'),
|
||||
type: 'primary',
|
||||
can: ({ row }) => {
|
||||
return (
|
||||
vm.$hasPerm('accounts.delete_account') &&
|
||||
!vm.$store.getters.currentOrgIsRoot &&
|
||||
!isDirectoryServiceAccount(row, vm)
|
||||
)
|
||||
},
|
||||
has: () => {
|
||||
return !vm.asset
|
||||
},
|
||||
callback: ({ row }) => {
|
||||
vm.accountCreateUpdateTitle = vm.$t('MoveToOther')
|
||||
vm.$route.query.flag = 'move'
|
||||
vm.iAsset = vm.asset
|
||||
vm.account = row
|
||||
vm.showAddDialog = true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const accountQuickFilters = vm => [
|
||||
{
|
||||
label: vm.$t('Recent (7 days)'),
|
||||
options: [
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<script>
|
||||
import TreeTable from '../../Table/TreeTable/index.vue'
|
||||
import { setRouterQuery, setUrlParam } from '@/utils/common'
|
||||
import { setRouterQuery, setUrlParam } from '@/utils/common/index'
|
||||
import $ from '@/utils/jquery-vendor'
|
||||
|
||||
export default {
|
||||
@@ -114,6 +114,11 @@ export default {
|
||||
treeUrl: `${this.typeUrl}?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
|
||||
callback: {
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
|
||||
},
|
||||
edit: {
|
||||
drag: {
|
||||
isMove: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,60 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="chat-action">
|
||||
<Select2
|
||||
v-model="select.value"
|
||||
:disabled="isLoading || isSelectDisabled"
|
||||
v-bind="select"
|
||||
@change="onSelectChange"
|
||||
/>
|
||||
<div class="model-select">
|
||||
<Select2
|
||||
v-model="select.value"
|
||||
:disabled="isLoading || isSelectDisabled || loading || !options.length"
|
||||
v-bind="select"
|
||||
@change="onSelectChange"
|
||||
/>
|
||||
</div>
|
||||
<el-dropdown
|
||||
:hide-on-click="false"
|
||||
trigger="click"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<i class="fa fa-plug" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<div class="menu-section">
|
||||
<div v-if="toolsLoading">
|
||||
<i class="el-icon-loading" /> {{ $t('Loading') }}
|
||||
</div>
|
||||
<div v-else class="menu-body">
|
||||
<div>
|
||||
<div
|
||||
v-for="item in toolOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
<div style="padding: 0 10px">
|
||||
<i class="fa fa-wrench item-icon" />
|
||||
<span class="item-label">{{ item.label }}</span>
|
||||
|
||||
<el-switch
|
||||
:value="selectedToolsSet.has(item.value)"
|
||||
@change="() => toggleTool(item.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in toolServerOptions"
|
||||
:key="item.value"
|
||||
>
|
||||
<div>
|
||||
<i class="fa fa-server item-icon" />
|
||||
<span class="item-label">{{ item.label }}</span>
|
||||
<el-switch
|
||||
:value="selectedToolServersSet.has(item.value)"
|
||||
@change="() => toggleToolServer(item.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<el-input
|
||||
@@ -36,6 +84,38 @@ export default {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modelOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedModel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
toolOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
toolServerOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedTools: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedToolServers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
toolsLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -43,15 +123,10 @@ export default {
|
||||
isIM: false,
|
||||
inputValue: '',
|
||||
select: {
|
||||
url: '/api/v1/settings/chatai-prompts/',
|
||||
value: '',
|
||||
multiple: false,
|
||||
placeholder: this.$t('Role'),
|
||||
ajax: {
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.content }
|
||||
}
|
||||
}
|
||||
placeholder: this.$t('Model'),
|
||||
options: []
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -60,7 +135,32 @@ export default {
|
||||
isLoading: state => state.chat.loading
|
||||
}),
|
||||
isSelectDisabled() {
|
||||
return !!this.select.value
|
||||
return false
|
||||
},
|
||||
options() {
|
||||
return (this.modelOptions || []).map(item => {
|
||||
return { label: item.name || item.id, value: item.id }
|
||||
})
|
||||
},
|
||||
selectedToolsSet() {
|
||||
return new Set(this.selectedTools || [])
|
||||
},
|
||||
selectedToolServersSet() {
|
||||
return new Set(this.selectedToolServers || [])
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelOptions: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.select.options = (val || []).map(item => ({ label: item.name || item.id, value: item.id }))
|
||||
}
|
||||
},
|
||||
selectedModel: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.select.value = val || ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -80,7 +180,25 @@ export default {
|
||||
this.inputValue = ''
|
||||
},
|
||||
onSelectChange(value) {
|
||||
this.$emit('select-prompt', value)
|
||||
this.$emit('select-model', value)
|
||||
},
|
||||
toggleTool(id) {
|
||||
const set = new Set(this.selectedTools || [])
|
||||
if (set.has(id)) {
|
||||
set.delete(id)
|
||||
} else {
|
||||
set.add(id)
|
||||
}
|
||||
this.$emit('select-tools', Array.from(set))
|
||||
},
|
||||
toggleToolServer(id) {
|
||||
const set = new Set(this.selectedToolServers || [])
|
||||
if (set.has(id)) {
|
||||
set.delete(id)
|
||||
} else {
|
||||
set.add(id)
|
||||
}
|
||||
this.$emit('select-tool-servers', Array.from(set))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,9 +213,18 @@ export default {
|
||||
.chat-action {
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.model-select {
|
||||
flex: 0 0 48%;
|
||||
max-width: 240px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&::v-deep .el-select {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
|
||||
.el-input__inner {
|
||||
height: 28px;
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
<div class="chart-item-container">
|
||||
<div class="avatar">
|
||||
<el-avatar
|
||||
:src="isUserRole ? userUrl : chatUrl"
|
||||
v-if="isUserRole"
|
||||
:src="userUrl"
|
||||
class="header-avatar"
|
||||
/>
|
||||
<el-avatar v-else class="header-avatar" :style="{ backgroundColor: 'transparent' }">
|
||||
<ModelIcon :name="modelIconName" class-name="model-icon" />
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="operational">
|
||||
@@ -33,7 +37,7 @@
|
||||
<!-- eslint-disable-next-line -->
|
||||
<div class="divider"></div>
|
||||
<p>
|
||||
<MessageText :message="item.reasoning" />
|
||||
<MessageText :message="item.reasoning" @insert-code="handleInsertCode" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -41,8 +45,7 @@
|
||||
<span v-if="isServerError" class="error">
|
||||
{{ isServerError }}
|
||||
</span>
|
||||
<MessageText :message="item.result" />
|
||||
</div>
|
||||
<MessageText :message="item.result" :is-terminal="isTerminal" @insert-code="handleInsertCode" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
@@ -77,22 +80,32 @@
|
||||
|
||||
<script>
|
||||
import MessageText from './MessageText.vue'
|
||||
import ModelIcon from '../../models/ModelIcon.vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { copy } from '@/utils/common'
|
||||
import { copy } from '@/utils/common/index'
|
||||
import { useChat } from '../../useChat.js'
|
||||
import { reconnect } from '@/utils/socket'
|
||||
import { reconnect } from '@/utils/request'
|
||||
|
||||
const { setLoading, removeLoadingMessageInChat } = useChat()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageText
|
||||
MessageText,
|
||||
ModelIcon
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
}
|
||||
},
|
||||
selectedModel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isTerminal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -126,10 +139,8 @@ export default {
|
||||
? this.$i18n.t('ServerBusyRetry')
|
||||
: ''
|
||||
},
|
||||
chatUrl() {
|
||||
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
|
||||
? require('@/assets/img/chat.png')
|
||||
: require('@/assets/img/deepSeek.png')
|
||||
modelIconName() {
|
||||
return (this.item?.message?.model || this.selectedModel || this.publicSettings.CHAT_AI_TYPE || '').toString()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -142,6 +153,9 @@ export default {
|
||||
if (value === 'copy') {
|
||||
copy(this.item.result.content)
|
||||
}
|
||||
},
|
||||
handleInsertCode(code) {
|
||||
this.$emit('insert-code', code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,11 +179,18 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
|
||||
&::v-deep img {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.model-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -17,7 +17,7 @@ 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'
|
||||
import { copy } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -25,6 +25,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => {
|
||||
}
|
||||
},
|
||||
isTerminal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -36,7 +40,7 @@ export default {
|
||||
text() {
|
||||
const value = this.message?.content || ''
|
||||
if (value && this.markdown) {
|
||||
return this.markdown?.render(value)
|
||||
return this.renderContentWithDetails(value)
|
||||
}
|
||||
return this.$xss.process(value)
|
||||
}
|
||||
@@ -45,10 +49,10 @@ export default {
|
||||
this.init()
|
||||
},
|
||||
updated() {
|
||||
this.addCopyEvents()
|
||||
this.addEvents()
|
||||
},
|
||||
destroyed() {
|
||||
this.removeCopyEvents()
|
||||
this.removeEvents()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
@@ -65,30 +69,139 @@ export default {
|
||||
return vm.highlightBlock(hljs.highlightAuto(code).value, '')
|
||||
}
|
||||
})
|
||||
this.markdown.use(mila, { attrs: { target: '_blank', rel: 'noopener', class: 'link-style' }})
|
||||
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>`
|
||||
let insertSpanHtml = `<span class="code-block-header__insert">${this.$t('Insert')}</span>`
|
||||
if (!this.isTerminal) {
|
||||
insertSpanHtml = ''
|
||||
}
|
||||
return `<pre class="code-block-wrapper">
|
||||
<div class="code-block-header">
|
||||
<span class="code-block-header__lang">${lang}</span>
|
||||
<span class="code-block-header__actions">
|
||||
${insertSpanHtml}
|
||||
<span class="code-block-header__copy">${this.$t('Copy')}</span>
|
||||
</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)
|
||||
}
|
||||
addEvents() {
|
||||
this.addBtnClickEvents('.code-block-header__copy', this.handlerClickCopy)
|
||||
this.addBtnClickEvents('.code-block-header__insert', this.handlerClickInsert)
|
||||
},
|
||||
|
||||
handlerClickCopy(event) {
|
||||
const wrapper = event.target.closest('.code-block-wrapper')
|
||||
if (wrapper) {
|
||||
// 查找里面的 code 元素
|
||||
const codeElement = wrapper.querySelector('code.code-block-body')
|
||||
if (codeElement) {
|
||||
const codeText = codeElement.textContent
|
||||
copy(codeText)
|
||||
}
|
||||
}
|
||||
},
|
||||
handlerClickInsert(event) {
|
||||
const wrapper = event.target.closest('.code-block-wrapper')
|
||||
if (wrapper) {
|
||||
// 查找里面的 code 元素
|
||||
const codeElement = wrapper.querySelector('code.code-block-body')
|
||||
if (codeElement) {
|
||||
const codeText = codeElement.textContent
|
||||
this.$emit('insert-code', codeText)
|
||||
}
|
||||
}
|
||||
},
|
||||
addBtnClickEvents(selector, callback) {
|
||||
const buttons = this.$refs.textRef.querySelectorAll(selector)
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', callback)
|
||||
})
|
||||
},
|
||||
renderContentWithDetails(value) {
|
||||
// Kael responses may wrap reasoning/thinking in <details type="reasoning">; render them with a custom block.
|
||||
const detailRegex = /<details[^>]*>[\s\S]*?<\/details>/gi
|
||||
let result = ''
|
||||
let lastIndex = 0
|
||||
let match
|
||||
let hasDetails = false
|
||||
|
||||
while ((match = detailRegex.exec(value))) {
|
||||
hasDetails = true
|
||||
const preceding = value.slice(lastIndex, match.index)
|
||||
if (preceding.trim()) {
|
||||
result += this.markdown.render(preceding)
|
||||
}
|
||||
result += this.renderDetailBlock(match[0])
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
|
||||
if (!hasDetails) {
|
||||
return this.markdown.render(value)
|
||||
}
|
||||
|
||||
const remaining = value.slice(lastIndex)
|
||||
if (remaining.trim()) {
|
||||
result += this.markdown.render(remaining)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
renderDetailBlock(detailStr) {
|
||||
const attributes = this.extractAttributes(detailStr)
|
||||
const inner = detailStr.replace(/^<details[^>]*>/i, '').replace(/<\/details>$/i, '')
|
||||
const summaryMatch = inner.match(/<summary>([\s\S]*?)<\/summary>/i)
|
||||
const summary = summaryMatch ? this.decodeHtml(summaryMatch[1]) : ''
|
||||
const body = summaryMatch ? inner.replace(summaryMatch[0], '') : inner
|
||||
const bodyHtml = body.trim() ? this.markdown.render(this.decodeHtml(body.trim())) : ''
|
||||
|
||||
const baseClass = 'kael-detail'
|
||||
if (attributes.type === 'reasoning') {
|
||||
const statusClass = attributes.done === 'true' ? 'is-done' : 'is-pending'
|
||||
const title = summary || this.$t('DeeplyThoughtAbout')
|
||||
return `<div class="${baseClass} ${baseClass}--reasoning ${statusClass}">
|
||||
<div class="${baseClass}__header">
|
||||
<span class="${baseClass}__status-dot"></span>
|
||||
<span class="${baseClass}__title">${title}</span>
|
||||
</div>
|
||||
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
|
||||
</div>`
|
||||
}
|
||||
|
||||
return `<div class="${baseClass}">
|
||||
${summary ? `<div class="${baseClass}__header">${summary}</div>` : ''}
|
||||
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
|
||||
</div>`
|
||||
},
|
||||
extractAttributes(detailStr) {
|
||||
const attrs = {}
|
||||
const attrMatch = detailStr.match(/^<details([^>]*)>/i)
|
||||
const attrStr = (attrMatch && attrMatch[1]) || ''
|
||||
attrStr.replace(/(\w+)="(.*?)"/g, (all, key, val) => {
|
||||
attrs[key] = val
|
||||
return all
|
||||
})
|
||||
return attrs
|
||||
},
|
||||
decodeHtml(str) {
|
||||
if (!str) return ''
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.innerHTML = str
|
||||
return textArea.value
|
||||
},
|
||||
removeBtnClickEvent(selector) {
|
||||
const buttons = this.$refs.textRef.querySelectorAll(selector)
|
||||
buttons.forEach((btn) => {
|
||||
btn.removeEventListener('click', () => {
|
||||
})
|
||||
})
|
||||
},
|
||||
removeCopyEvents() {
|
||||
removeEvents() {
|
||||
if (this.$refs.textRef) {
|
||||
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.removeEventListener('click', () => {
|
||||
})
|
||||
})
|
||||
this.removeBtnClickEvent('.code-block-header__copy')
|
||||
this.addBtnClickEvents('.code-block-header__insert')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +211,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
font-size: 13px;
|
||||
max-width: 300px;;
|
||||
|
||||
&::v-deep p {
|
||||
margin-bottom: 0 !important;
|
||||
@@ -115,26 +229,46 @@ export default {
|
||||
|
||||
&::v-deep .code-block-wrapper {
|
||||
background: #1F2329;
|
||||
padding: 2px 6px;
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.code-block-body {
|
||||
padding: 5px 10px 0;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
;
|
||||
|
||||
.code-block-header {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
background: #353946;
|
||||
color: #c2d1e1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.code-block-header__copy {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
.code-block-header__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
color: #6e747b;
|
||||
.code-block-header__copy {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #6e747b;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block-header__insert {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #6e747b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +312,7 @@ export default {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -194,4 +329,64 @@ export default {
|
||||
.loading-box span:nth-child(3) {
|
||||
animation-delay: 0.49s;
|
||||
}
|
||||
|
||||
.kael-detail {
|
||||
margin: 8px 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
background: #f7f8fa;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 6px;
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid #e5e5e5;
|
||||
}
|
||||
|
||||
&--reasoning.is-pending {
|
||||
border-color: #f59e0b40;
|
||||
background: #fff8e6;
|
||||
|
||||
.kael-detail__status-dot {
|
||||
background: #f59e0b;
|
||||
animation: kael-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&--reasoning.is-done {
|
||||
border-color: #dbeafe;
|
||||
background: #f4f6ff;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kael-pulse {
|
||||
0% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BASE_URL } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
active: {
|
||||
@@ -41,6 +43,16 @@ export default {
|
||||
},
|
||||
handleExpand() {
|
||||
this.$emit('expand-full')
|
||||
},
|
||||
async openWebsite() {
|
||||
let url = `${BASE_URL}/?_=${Date.now()}`
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
url = url.replace('9528', '5173')
|
||||
}
|
||||
|
||||
const newUrl = new URL(url)
|
||||
window.open(newUrl.toString(), '_blank')
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<DrawerPanel
|
||||
v-if="visible"
|
||||
ref="drawer"
|
||||
:default-show-panel="!!defaultShowPanel"
|
||||
:expanded="expanded"
|
||||
@@ -45,9 +46,11 @@
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import Chat from './components/ChitChat/index.vue'
|
||||
import { getInputFocus } from './useChat.js'
|
||||
import { ws } from '@/utils/socket'
|
||||
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
|
||||
import { ObjectLocalStorage } from '@/utils/common'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const aiPannelLocalStorage = new ObjectLocalStorage('ai_panel_settings')
|
||||
export default {
|
||||
components: {
|
||||
DrawerPanel,
|
||||
@@ -72,23 +75,76 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
active: 'chat',
|
||||
robotUrl: require('@/assets/img/robot-assistant.png'),
|
||||
height: '400px',
|
||||
expanded: false,
|
||||
clientOffset: {}
|
||||
clientOffset: {},
|
||||
currentTerminalContent: {},
|
||||
initialized: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'publicSettings'
|
||||
])
|
||||
},
|
||||
watch: {
|
||||
'publicSettings.CHAT_AI_METHOD': {
|
||||
handler(newVal) {
|
||||
this.visible = newVal === 'api'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handlePostMessage()
|
||||
this.handleStartChat()
|
||||
},
|
||||
methods: {
|
||||
handleStartChat() {
|
||||
if (this.publicSettings.CHAT_AI_METHOD === 'api') {
|
||||
this.visible = true
|
||||
const expanded = aiPannelLocalStorage.get('expanded')
|
||||
this.updateExpandedState(expanded)
|
||||
this.handlePostMessage()
|
||||
} else if (this.publicSettings.CHAT_AI_METHOD === 'embed') {
|
||||
const embedScriptId = 'chat-ai-embed-id'
|
||||
if (document.getElementById(embedScriptId)) {
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.id = embedScriptId
|
||||
script.src = this.publicSettings.CHAT_AI_EMBED_URL
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
const loadEvent = new Event('load', { bubbles: false, cancelable: false })
|
||||
window.dispatchEvent(loadEvent)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
},
|
||||
initAssistant() {
|
||||
if (this.initialized) return
|
||||
this.initialized = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.component?.init()
|
||||
})
|
||||
},
|
||||
handlePostMessage() {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data === 'show-chat-panel') {
|
||||
this.$refs.drawer.show = true
|
||||
this.initAssistant()
|
||||
return
|
||||
}
|
||||
const msg = event.data
|
||||
switch (msg.name) {
|
||||
case 'current_terminal_content':
|
||||
// {content: '...', terminalId: '',sessionId: '',viewId: '',viewName: ''}
|
||||
this.$log.debug('current_terminal_content', msg)
|
||||
this.currentTerminalContent = msg.data
|
||||
this.$refs.component?.onTerminalContext(msg.data)
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -96,23 +152,30 @@ export default {
|
||||
this.$refs.drawer.handleHeaderMoveDown(event)
|
||||
},
|
||||
handleMouseMoveUp(event) {
|
||||
this.$refs.drawer.handleHeaderMoveUp(event)
|
||||
},
|
||||
initWebSocket() {
|
||||
if (!ws) {
|
||||
this.$refs.component?.init()
|
||||
// Prevent the new chat button from triggering the header move up
|
||||
const newButton = event.target.closest('.new')
|
||||
if (newButton) {
|
||||
return
|
||||
}
|
||||
this.$refs.drawer.handleHeaderMoveUp(event)
|
||||
},
|
||||
onClose() {
|
||||
this.$refs.drawer.show = false
|
||||
},
|
||||
expandFull() {
|
||||
this.height = '100%'
|
||||
this.expanded = true
|
||||
this.updateExpandedState(true)
|
||||
this.save_pannel_settings()
|
||||
},
|
||||
compress() {
|
||||
this.height = '400px'
|
||||
this.expanded = false
|
||||
this.updateExpandedState(false)
|
||||
this.save_pannel_settings()
|
||||
},
|
||||
save_pannel_settings() {
|
||||
aiPannelLocalStorage.set('expanded', this.expanded)
|
||||
},
|
||||
updateExpandedState(expanded) {
|
||||
this.expanded = expanded
|
||||
this.height = expanded ? '100%' : '400px'
|
||||
},
|
||||
onNewChat() {
|
||||
this.active = 'chat'
|
||||
@@ -122,8 +185,8 @@ export default {
|
||||
})
|
||||
},
|
||||
onToggle(status) {
|
||||
this.initWebSocket()
|
||||
if (status) {
|
||||
this.initAssistant()
|
||||
getInputFocus()
|
||||
}
|
||||
}
|
||||
|
||||
33
src/components/Apps/ChatAi/models/ChatGPT.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg
|
||||
:stroke-width="strokeWidth"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="className"
|
||||
>
|
||||
<path
|
||||
fill="#6b7280"
|
||||
style="fill: #6b7280 !important"
|
||||
d="M11.2475 18.25C10.6975 18.25 10.175 18.1455 9.67999 17.9365C9.18499 17.7275 8.74499 17.436 8.35999 17.062C7.94199 17.205 7.50749 17.2765 7.05649 17.2765C6.31949 17.2765 5.63749 17.095 5.01049 16.732C4.38349 16.369 3.87749 15.874 3.49249 15.247C3.11849 14.62 2.93149 13.9215 2.93149 13.1515C2.93149 12.8325 2.97549 12.486 3.06349 12.112C2.62349 11.705 2.28249 11.2375 2.04049 10.7095C1.79849 10.1705 1.67749 9.6095 1.67749 9.0265C1.67749 8.4325 1.80399 7.8605 2.05699 7.3105C2.30999 6.7605 2.66199 6.2875 3.11299 5.8915C3.57499 5.4845 4.10849 5.204 4.71349 5.05C4.83449 4.423 5.08749 3.862 5.47249 3.367C5.86849 2.861 6.35249 2.465 6.92449 2.179C7.49649 1.893 8.10699 1.75 8.75599 1.75C9.30599 1.75 9.82849 1.8545 10.3235 2.0635C10.8185 2.2725 11.2585 2.564 11.6435 2.938C12.0615 2.795 12.496 2.7235 12.947 2.7235C13.684 2.7235 14.366 2.905 14.993 3.268C15.62 3.631 16.1205 4.126 16.4945 4.753C16.8795 5.38 17.072 6.0785 17.072 6.8485C17.072 7.1675 17.028 7.514 16.94 7.888C17.38 8.295 17.721 8.768 17.963 9.307C18.205 9.835 18.326 10.3905 18.326 10.9735C18.326 11.5675 18.1995 12.1395 17.9465 12.6895C17.6935 13.2395 17.336 13.718 16.874 14.125C16.423 14.521 15.895 14.796 15.29 14.95C15.169 15.577 14.9105 16.138 14.5145 16.633C14.1295 17.139 13.651 17.535 13.079 17.821C12.507 18.107 11.8965 18.25 11.2475 18.25ZM7.17199 16.1875C7.72199 16.1875 8.20049 16.072 8.60749 15.841L11.7095 14.059C11.8195 13.982 11.8745 13.8775 11.8745 13.7455V12.3265L7.88149 14.62C7.63949 14.763 7.39749 14.763 7.15549 14.62L4.03699 12.8215C4.03699 12.8545 4.03149 12.893 4.02049 12.937C4.02049 12.981 4.02049 13.047 4.02049 13.135C4.02049 13.696 4.15249 14.213 4.41649 14.686C4.69149 15.148 5.07099 15.511 5.55499 15.775C6.03899 16.05 6.57799 16.1875 7.17199 16.1875ZM7.33699 13.498C7.40299 13.531 7.46349 13.5475 7.51849 13.5475C7.57349 13.5475 7.62849 13.531 7.68349 13.498L8.92099 12.7885L4.94449 10.4785C4.70249 10.3355 4.58149 10.121 4.58149 9.835V6.2545C4.03149 6.4965 3.59149 6.8705 3.26149 7.3765C2.93149 7.8715 2.76649 8.4215 2.76649 9.0265C2.76649 9.5655 2.90399 10.0825 3.17899 10.5775C3.45399 11.0725 3.81149 11.4465 4.25149 11.6995L7.33699 13.498ZM11.2475 17.161C11.8305 17.161 12.3585 17.029 12.8315 16.765C13.3045 16.501 13.6785 16.138 13.9535 15.676C14.2285 15.214 14.366 14.697 14.366 14.125V10.561C14.366 10.429 14.311 10.33 14.201 10.264L12.947 9.538V14.1415C12.947 14.4275 12.826 14.642 12.584 14.785L9.46549 16.5835C10.0045 16.9685 10.5985 17.161 11.2475 17.161ZM11.8745 11.122V8.878L10.01 7.822L8.12899 8.878V11.122L10.01 12.178L11.8745 11.122ZM7.05649 5.8585C7.05649 5.5725 7.17749 5.358 7.41949 5.215L10.538 3.4165C9.99899 3.0315 9.40499 2.839 8.75599 2.839C8.17299 2.839 7.64499 2.971 7.17199 3.235C6.69899 3.499 6.32499 3.862 6.04999 4.324C5.78599 4.786 5.65399 5.303 5.65399 5.875V9.4225C5.65399 9.5545 5.70899 9.659 5.81899 9.736L7.05649 10.462V5.8585ZM15.4385 13.7455C15.9885 13.5035 16.423 13.1295 16.742 12.6235C17.072 12.1175 17.237 11.5675 17.237 10.9735C17.237 10.4345 17.0995 9.9175 16.8245 9.4225C16.5495 8.9275 16.192 8.5535 15.752 8.3005L12.6665 6.5185C12.6005 6.4745 12.54 6.458 12.485 6.469C12.43 6.469 12.375 6.4855 12.32 6.5185L11.0825 7.2115L15.0755 9.538C15.1965 9.604 15.2845 9.692 15.3395 9.802C15.4055 9.901 15.4385 10.022 15.4385 10.165V13.7455ZM12.122 5.3635C12.364 5.2095 12.606 5.2095 12.848 5.3635L15.983 7.195C15.983 7.118 15.983 7.019 15.983 6.898C15.983 6.37 15.851 5.8695 15.587 5.3965C15.334 4.9125 14.9655 4.5275 14.4815 4.2415C14.0085 3.9555 13.4585 3.8125 12.8315 3.8125C12.2815 3.8125 11.803 3.928 11.396 4.159L8.29399 5.941C8.18399 6.018 8.12899 6.1225 8.12899 6.2545V7.6735L12.122 5.3635Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChatGPTIcon',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-8'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
45
src/components/Apps/ChatAi/models/Claude.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<svg
|
||||
:stroke-width="strokeWidth"
|
||||
viewBox="0 0 24 16"
|
||||
overflow="visible"
|
||||
width="20"
|
||||
height="20"
|
||||
:class="className"
|
||||
>
|
||||
<g style="transform: translateX(13px) rotateZ(0deg); transform-origin: 4.775px 7.73501px;">
|
||||
<path
|
||||
shape-rendering="geometricPrecision"
|
||||
fill-opacity="1"
|
||||
fill="#8c653f"
|
||||
style="fill: #8c653f !important"
|
||||
d=" M0,0 C0,0 6.1677093505859375,15.470022201538086 6.1677093505859375,15.470022201538086 C6.1677093505859375,15.470022201538086 9.550004005432129,15.470022201538086 9.550004005432129,15.470022201538086 C9.550004005432129,15.470022201538086 3.382294178009033,0 3.382294178009033,0 C3.382294178009033,0 0,0 0,0 C0,0 0,0 0,0z"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="1" style="transform: none; transform-origin: 7.935px 7.73501px;">
|
||||
<path
|
||||
shape-rendering="geometricPrecision"
|
||||
fill-opacity="1"
|
||||
fill="#8c653f"
|
||||
style="fill: #8c653f !important"
|
||||
d=" M5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 7.93500280380249,3.911694288253784 7.93500280380249,3.911694288253784 C7.93500280380249,3.911694288253784 10.045400619506836,9.348296165466309 10.045400619506836,9.348296165466309 C10.045400619506836,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309z M6.166755199432373,0 C6.166755199432373,0 0,15.470022201538086 0,15.470022201538086 C0,15.470022201538086 3.4480772018432617,15.470022201538086 3.4480772018432617,15.470022201538086 C3.4480772018432617,15.470022201538086 4.709278583526611,12.22130012512207 4.709278583526611,12.22130012512207 C4.709278583526611,12.22130012512207 11.16093635559082,12.22130012512207 11.16093635559082,12.22130012512207 C11.16093635559082,12.22130012512207 12.421928405761719,15.470022201538086 12.421928405761719,15.470022201538086 C12.421928405761719,15.470022201538086 15.87000560760498,15.470022201538086 15.87000560760498,15.470022201538086 C15.87000560760498,15.470022201538086 9.703250885009766,0 9.703250885009766,0 C9.703250885009766,0 6.166755199432373,0 6.166755199432373,0 C6.166755199432373,0 6.166755199432373,0 6.166755199432373,0z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClaudeIcon',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-4'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
34
src/components/Apps/ChatAi/models/DeepSeek.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<svg
|
||||
id="图层_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-name="图层 1"
|
||||
viewBox="0 0 71.69 52.76"
|
||||
:class="className"
|
||||
:stroke-width="strokeWidth"
|
||||
>
|
||||
<path
|
||||
id="path"
|
||||
fill="#4d6bfe"
|
||||
style="fill: #4d6bfe !important"
|
||||
d="M523.77,276.34c-.76-.38-1.08.33-1.53.69a4,4,0,0,0-.41.41,5.07,5.07,0,0,1-4.1,1.87,8,8,0,0,0-6.46,2.53,5.82,5.82,0,0,0-3.72-4.62,6.39,6.39,0,0,1-2.85-1.94,7.76,7.76,0,0,1-.92-2.31c-.16-.48-.32-1-.87-1.05s-.83.41-1.07.82a11,11,0,0,0-1.26,5.5,11.9,11.9,0,0,0,5.49,10.14.75.75,0,0,1,.39,1c-.25.84-.54,1.65-.79,2.49-.17.53-.41.65-1,.42a16.63,16.63,0,0,1-5.18-3.52c-2.56-2.48-4.88-5.21-7.76-7.35-.68-.5-1.36-1-2.06-1.41-2.94-2.86.39-5.2,1.16-5.48s.28-1.29-2.33-1.28-5,.88-8,2a8.23,8.23,0,0,1-1.39.41,28.67,28.67,0,0,0-8.61-.3,18.57,18.57,0,0,0-13.44,7.83c-4,5.47-4.91,11.67-3.76,18.15a27.68,27.68,0,0,0,10,16.88,26.8,26.8,0,0,0,19.23,6.39c4.43-.25,9.36-.84,14.92-5.55a13.84,13.84,0,0,0,5.32,1.18,17.24,17.24,0,0,0,5.09-.38c2.2-.46,2.05-2.5,1.25-2.87-6.43-3-5-1.78-6.3-2.77,3.27-3.87,8.2-7.89,10.13-20.92a12.44,12.44,0,0,0,0-2.52c0-.51.1-.71.68-.76a12.55,12.55,0,0,0,4.62-1.42c4.17-2.28,5.85-6,6.25-10.51A1.57,1.57,0,0,0,523.77,276.34Zm-36.34,40.37c-6.24-4.9-9.27-6.52-10.52-6.45s-1,1.41-.7,2.28a8.49,8.49,0,0,0,1.11,2.21,1.14,1.14,0,0,1-.34,1.8c-2,1.24-5.5-.42-5.66-.5a26.08,26.08,0,0,1-9.87-9.88,30.15,30.15,0,0,1-3.87-13.39c-.06-1.15.28-1.56,1.42-1.77a14.31,14.31,0,0,1,4.57-.11,28.56,28.56,0,0,1,16.33,8.29,54.06,54.06,0,0,1,6.58,8.63,41.46,41.46,0,0,0,7.41,8.71,24.36,24.36,0,0,0,2.66,2C494.16,318.82,490.16,318.87,487.43,316.71Zm3-19.23a.92.92,0,0,1,.92-.92.83.83,0,0,1,.32.06.8.8,0,0,1,.34.22.9.9,0,0,1,.25.64.92.92,0,0,1-1.83,0Zm9.29,4.76a5.27,5.27,0,0,1-1.77.48,3.75,3.75,0,0,1-2.38-.76,3.57,3.57,0,0,1-1.65-2.26,5.16,5.16,0,0,1,0-1.76,2,2,0,0,0-.71-2.17,3.1,3.1,0,0,0-2.06-.59,1.63,1.63,0,0,1-.76-.24.75.75,0,0,1-.34-1.07,3.47,3.47,0,0,1,.57-.62,3.9,3.9,0,0,1,3.43,0,10,10,0,0,1,3,2.34,18.62,18.62,0,0,1,2,2.73,10.9,10.9,0,0,1,1.33,2.53C500.65,301.47,500.4,302,499.71,302.24Z"
|
||||
transform="translate(-452.83 -271.91)"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeepSeekIcon',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-4'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
33
src/components/Apps/ChatAi/models/Gemini.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 512 512"
|
||||
:class="className"
|
||||
:stroke-width="strokeWidth"
|
||||
>
|
||||
<path
|
||||
fill="#4285f4"
|
||||
style="fill: #4285f4 !important"
|
||||
d="M473.16,221.48l-2.26-9.59H262.46v88.22H387c-12.93,61.4-72.93,93.72-121.94,93.72-35.66,0-73.25-15-98.13-39.11a140.08,140.08,0,0,1-41.8-98.88c0-37.16,16.7-74.33,41-98.78s61-38.13,97.49-38.13c41.79,0,71.74,22.19,82.94,32.31l62.69-62.36C390.86,72.72,340.34,32,261.6,32h0c-60.75,0-119,23.27-161.58,65.71C58,139.5,36.25,199.93,36.25,256S56.83,369.48,97.55,411.6C141.06,456.52,202.68,480,266.13,480c57.73,0,112.45-22.62,151.45-63.66,38.34-40.4,58.17-96.3,58.17-154.9C475.75,236.77,473.27,222.12,473.16,221.48Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GeminiIcon',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-4'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
33
src/components/Apps/ChatAi/models/Grok.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
fill="currentColor"
|
||||
:class="className"
|
||||
:stroke-width="strokeWidth"
|
||||
>
|
||||
<path
|
||||
fill="#000000"
|
||||
style="fill: #000000 !important"
|
||||
d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GrokIcon',
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-4'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
50
src/components/Apps/ChatAi/models/ModelIcon.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<component
|
||||
:is="resolvedIcon"
|
||||
v-if="resolvedIcon"
|
||||
:class-name="className"
|
||||
:stroke-width="strokeWidth"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatGPTIcon from './ChatGPT.vue'
|
||||
import DeepSeekIcon from './DeepSeek.vue'
|
||||
import GrokIcon from './Grok.vue'
|
||||
import ClaudeIcon from './Claude.vue'
|
||||
import GeminiIcon from './Gemini.vue'
|
||||
|
||||
export default {
|
||||
name: 'ModelIcon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: 'size-5'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: [String, Number],
|
||||
default: '1.5'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
normalizedName() {
|
||||
return (this.name || '').toLowerCase()
|
||||
},
|
||||
resolvedIcon() {
|
||||
const name = this.normalizedName
|
||||
if (!name) return null
|
||||
if (name.includes('gpt')) return ChatGPTIcon
|
||||
if (name.includes('deep-seek')) return DeepSeekIcon
|
||||
if (name.includes('deepseek')) return DeepSeekIcon
|
||||
if (name.includes('grok')) return GrokIcon
|
||||
if (name.includes('claude')) return ClaudeIcon
|
||||
if (name.includes('gemini')) return GeminiIcon
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
import store from '@/store'
|
||||
import { pageScroll } from '@/utils/common'
|
||||
import { pageScroll } from '@/utils/common/index'
|
||||
|
||||
export const getInputFocus = () => {
|
||||
const dom = document.querySelector('.chat-input .el-textarea__inner')
|
||||
@@ -9,11 +9,11 @@ export const getInputFocus = () => {
|
||||
export function useChat() {
|
||||
const chatStore = {}
|
||||
|
||||
const setLoading = (loading) => {
|
||||
const setLoading = loading => {
|
||||
store.commit('chat/setLoading', loading)
|
||||
}
|
||||
|
||||
const onNewChat = (name) => {
|
||||
const onNewChat = name => {
|
||||
const data = {
|
||||
name: name || `new chat`,
|
||||
id: 1,
|
||||
@@ -27,7 +27,7 @@ export function useChat() {
|
||||
store.commit('chat/clearChats')
|
||||
}
|
||||
|
||||
const addMessageToActiveChat = (chat) => {
|
||||
const addMessageToActiveChat = chat => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useChat() {
|
||||
store.commit('chat/removeLoadingMessageInChat')
|
||||
}
|
||||
|
||||
const addChatMessageById = (chat) => {
|
||||
const addChatMessageById = chat => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
if (chat?.conversation_id) {
|
||||
store.commit('chat/setActiveChatConversationId', chat.conversation_id)
|
||||
@@ -54,7 +54,7 @@ export function useChat() {
|
||||
addChatMessageById(temporaryChat)
|
||||
}
|
||||
|
||||
const newChatAndAddMessageById = (chat) => {
|
||||
const newChatAndAddMessageById = chat => {
|
||||
onNewChat(chat.message.content)
|
||||
addChatMessageById(chat)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import { openTaskPage } from '@/utils/jms/index'
|
||||
|
||||
export default {
|
||||
name: 'GatewayDialog',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import AssetTreeTable from '@/components/Apps/AssetTreeTable'
|
||||
import { AccountInfoFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
|
||||
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
|
||||
import { setUrlParam } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
name: 'GrantedAssets',
|
||||
@@ -34,7 +35,7 @@ export default {
|
||||
}
|
||||
const initialUrl = vm.tableConfig.initialUrl
|
||||
const nodeId = node.meta.data.id
|
||||
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
|
||||
const url = setUrlParam(initialUrl, 'node_id', nodeId)
|
||||
vm.tableConfig.url = url
|
||||
}
|
||||
},
|
||||
@@ -70,7 +71,7 @@ export default {
|
||||
showMenu: false,
|
||||
showRefresh: true,
|
||||
showAssets: false,
|
||||
showSearch: false,
|
||||
showSearch: true,
|
||||
url: this.tableUrl,
|
||||
// ?assets=0不显示资产. =1显示资产
|
||||
treeUrl: this.treeUrl,
|
||||
@@ -78,6 +79,9 @@ export default {
|
||||
callback: {
|
||||
onSelected: (event, node) => vm.onSelected(node, vm),
|
||||
refresh: vm.refreshObjectAssetPermission
|
||||
},
|
||||
async: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
tableConfig: {
|
||||
@@ -85,6 +89,7 @@ export default {
|
||||
hasTree: true,
|
||||
columnsExtra: ['view_account'],
|
||||
columnsExclude: ['spec_info'],
|
||||
columns: ['id', 'name', 'address', 'comment', 'labels', 'connectivity', 'platform', 'view_account', 'actions'],
|
||||
columnsShow: {
|
||||
min: ['name', 'address', 'accounts'],
|
||||
default: ['name', 'address', 'platform', 'view_account', 'actions']
|
||||
@@ -108,11 +113,6 @@ export default {
|
||||
},
|
||||
connectivity: connectivityMeta,
|
||||
comment: { ...this.comment }
|
||||
},
|
||||
tableAttrs: {
|
||||
rowClassName({ row }) {
|
||||
return !row.is_active ? 'row_disabled' : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
|
||||
63
src/components/Apps/LeakPasswords/LeakPasswordList.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<ListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" :create-drawer="createDrawer" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DrawerListTable as ListTable } from '@/components'
|
||||
|
||||
export default {
|
||||
name: 'LeakPasswordList',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
createDrawer: () => import('@/components/Apps/LeakPasswords/LeakPasswordsCreateUpdate.vue'),
|
||||
tableConfig: {
|
||||
url: '/api/v1/settings/leak-passwords/',
|
||||
columns: [
|
||||
'password'
|
||||
],
|
||||
columnsMeta: {
|
||||
actions: {
|
||||
formatterArgs: {
|
||||
hasClone: false,
|
||||
canDelete: this.$hasPerm('settings.change_security'),
|
||||
canUpdate: this.$hasPerm('settings.change_security')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
headerActions: {
|
||||
hasExport: true,
|
||||
hasImport: true,
|
||||
importOptions: {
|
||||
encryptFields: [''], // 这里不加密 password,''只是为了保证数组有值
|
||||
canImportUpdate: false
|
||||
},
|
||||
hasCreate: true,
|
||||
hasSearch: true,
|
||||
hasRefresh: true,
|
||||
hasBulkDelete: true,
|
||||
hasBulkUpdate: false,
|
||||
hasLeftActions: true,
|
||||
hasRightActions: true,
|
||||
canCreate: this.$hasPerm('settings.change_security'),
|
||||
canBulkDelete: this.$hasPerm('settings.change_security')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<GenericCreateUpdatePage
|
||||
v-if="!loading"
|
||||
class="user-create-update"
|
||||
v-bind="$data"
|
||||
:title="null"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GenericCreateUpdatePage } from '@/layout/components'
|
||||
|
||||
export default {
|
||||
name: 'LeakPasswordsCreateUpdate',
|
||||
components: {
|
||||
GenericCreateUpdatePage
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
fields: [
|
||||
[this.$t('Basic'), ['password']]
|
||||
],
|
||||
encryptedFields: [],
|
||||
url: '/api/v1/settings/leak-passwords/'
|
||||
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = false
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
76
src/components/Apps/LeakPasswords/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>
|
||||
{{ $tc('View') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
v-bind="$attrs"
|
||||
width="40%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<LeakPasswordList />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@/components'
|
||||
import LeakPasswordList from '@/components/Apps/LeakPasswords/LeakPasswordList.vue'
|
||||
|
||||
export default {
|
||||
componentName: 'LeakPasswords',
|
||||
components: {
|
||||
LeakPasswordList,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('LeakPasswordList')
|
||||
}
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: `/api/v1/settings/leak-passwords/`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: this.value,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
hasButtons: true,
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
import { DrawerListTable as ListTable } from '@/components'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
import { toM2MJsonParams } from '@/utils/jms/index'
|
||||
import { DetailFormatter } from '@/components/Table/TableFormatters'
|
||||
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
|
||||
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.assets)
|
||||
const org_id = this.object.org_id || this.$store.getters.currentOrg.id
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/assets/assets/?${key}=${value}`,
|
||||
url: `/api/v1/assets/assets/?${key}=${value}&oid=${org_id}`,
|
||||
columns: ['name', 'address', 'platform', 'type', 'is_active'],
|
||||
columnsShow: {
|
||||
min: ['name', 'address'],
|
||||
@@ -45,7 +46,7 @@ export default {
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
getRoute: ({ row }) => {
|
||||
return { name: 'AssetDetail', params: { id: row.id }}
|
||||
return { name: 'AssetDetail', params: { id: row.id } }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
import { DrawerListTable as ListTable } from '@/components'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
import { toM2MJsonParams } from '@/utils/jms/index'
|
||||
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
|
||||
import { DetailFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.users)
|
||||
const org_id = this.object.org_id || this.$store.getters.currentOrg.id
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/users/users/?${key}=${value}`,
|
||||
url: `/api/v1/users/users/?${key}=${value}&oid=${org_id}`,
|
||||
columns: [
|
||||
'name', 'username', 'email', 'groups', 'system_roles',
|
||||
'org_roles', 'source', 'is_valid'
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<script>
|
||||
import IBox from '@/components/Common/IBox/index.vue'
|
||||
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import { toSafeLocalDateStr } from '@/utils/time'
|
||||
import { openTaskPage } from '@/utils/jms/index'
|
||||
import { toSafeLocalDateStr } from '@/utils/common/time'
|
||||
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
|
||||
|
||||
export default {
|
||||
|
||||
@@ -8,22 +8,17 @@
|
||||
:visible.sync="visible"
|
||||
class="dialog-content"
|
||||
v-bind="$attrs"
|
||||
width="740px"
|
||||
width="600px"
|
||||
@confirm="visible = false"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div v-if="confirmTypeRequired === 'relogin'">
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-row :gutter="24" style="margin: 0 auto">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-alert
|
||||
:title="$tc('ReLoginTitle')"
|
||||
center
|
||||
style="margin-bottom: 20px;"
|
||||
type="error"
|
||||
/>
|
||||
<el-alert :title="$tc('ReLoginTitle')" center style="margin-bottom: 20px" type="error" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-row :gutter="24" style="margin: 0 auto">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
|
||||
{{ this.$t('ReLogin') }}
|
||||
@@ -32,11 +27,11 @@
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-row :gutter="24" style="margin: 0 auto">
|
||||
<el-col :md="24" :sm="24" :span="24" class="add">
|
||||
<el-select
|
||||
v-model="subTypeSelected"
|
||||
style="width: 100%; margin-bottom: 20px;"
|
||||
style="width: 100%; margin-bottom: 20px"
|
||||
@change="handleSubTypeChange"
|
||||
>
|
||||
<el-option
|
||||
@@ -49,41 +44,46 @@
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<el-row v-if="!noCodeMFA.includes(subTypeSelected)" :gutter="24" style="margin: 0 auto">
|
||||
<el-col :md="24" :sm="24" style="display: flex; align-items: center">
|
||||
<el-input
|
||||
v-if="subTypeSelected !== 'face'"
|
||||
v-model="secretValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:show-password="showPassword"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
|
||||
<iframe
|
||||
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
|
||||
:src="faceCaptureUrl"
|
||||
allow="camera"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
style="width: 100%; height: 800px;border: none;"
|
||||
/>
|
||||
|
||||
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
|
||||
<span
|
||||
v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'"
|
||||
style="margin: -1px 0 0 20px"
|
||||
>
|
||||
<el-button
|
||||
:disabled="smsBtnDisabled"
|
||||
size="mini"
|
||||
style="line-height: 14px; float: right;"
|
||||
style="line-height: 14px; float: right"
|
||||
type="primary"
|
||||
@click="sendSMSCode"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ smsBtnText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 10px auto;">
|
||||
<el-row>
|
||||
<el-col>
|
||||
<iframe v-if="passkeyVisible" :src="passkeyUrl" style="display: none" />
|
||||
<iframe
|
||||
v-if="isFaceCaptureVisible && subTypeSelected === 'face' && faceCaptureUrl"
|
||||
:src="faceCaptureUrl"
|
||||
allow="camera"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
style="width: 100%; height: 600px; border: none"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 20px auto 10px">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button
|
||||
v-if="subTypeSelected!=='face'"
|
||||
v-if="!noCodeMFA.includes(subTypeSelected)"
|
||||
class="confirm-btn"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@@ -92,7 +92,8 @@
|
||||
{{ this.$t('Confirm') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="subTypeSelected==='face'&&!isFaceCaptureVisible"
|
||||
v-if="subTypeSelected === 'face'"
|
||||
v-show="!isFaceCaptureVisible"
|
||||
class="confirm-btn"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@@ -100,6 +101,16 @@
|
||||
>
|
||||
{{ this.$tc('VerifyFace') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="subTypeSelected === 'passkey'"
|
||||
v-loading="passkeyVisible"
|
||||
class="confirm-btn"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="handlePasskeyVerify"
|
||||
>
|
||||
{{ this.$tc('Next') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -107,7 +118,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
|
||||
export default {
|
||||
name: 'UserConfirmDialog',
|
||||
@@ -141,7 +152,11 @@ export default {
|
||||
processing: false,
|
||||
isFaceCaptureVisible: false,
|
||||
faceToken: null,
|
||||
faceCaptureUrl: null
|
||||
faceCaptureUrl: null,
|
||||
noCodeMFA: ['face', 'passkey'],
|
||||
sendCodeMFA: ['email', 'sms', 'otp'],
|
||||
passkeyVisible: false,
|
||||
passkeyUrl: '/api/v1/authentication/passkeys/login/?mfa=1'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -174,79 +189,119 @@ export default {
|
||||
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
|
||||
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('NeedReLogin')
|
||||
this.visible = true
|
||||
})
|
||||
return
|
||||
}
|
||||
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((err) => {
|
||||
const data = err.response?.data
|
||||
const msg = data?.error || data?.detail || data?.msg || this.$t('GetConfirmTypeFailed')
|
||||
this.$message.error(msg)
|
||||
this.cancel(err)
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
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('NeedReLogin')
|
||||
this.visible = true
|
||||
})
|
||||
return
|
||||
}
|
||||
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(err => {
|
||||
const data = err.response?.data
|
||||
const msg = data?.error || data?.detail || data?.msg || this.$t('GetConfirmTypeFailed')
|
||||
this.$message.error(msg)
|
||||
this.cancel(err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}, 500),
|
||||
logout() {
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
},
|
||||
sendSMSCode() {
|
||||
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: 'sms' }).then(res => {
|
||||
this.$message.success(this.$tc('VerificationCodeSent'))
|
||||
let time = 60
|
||||
this.smsBtnDisabled = true
|
||||
sendCode() {
|
||||
this.$axios
|
||||
.post(`/api/v1/authentication/mfa/select/`, { type: this.subTypeSelected })
|
||||
.then(res => {
|
||||
this.$message.success(this.$tc('VerificationCodeSent'))
|
||||
let time = 60
|
||||
this.smsBtnDisabled = true
|
||||
|
||||
const interval = setInterval(() => {
|
||||
time -= 1
|
||||
this.smsBtnText = `${this.$t('Pending')}: ${time}`
|
||||
const interval = setInterval(() => {
|
||||
time -= 1
|
||||
this.smsBtnText = `${this.$t('Pending')}: ${time}`
|
||||
|
||||
if (time <= 0) {
|
||||
clearInterval(interval)
|
||||
this.smsBtnText = this.$t('SendVerificationCode')
|
||||
this.smsBtnDisabled = false
|
||||
}
|
||||
}, 1000)
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$tc('FailedToSendVerificationCode'))
|
||||
})
|
||||
if (time <= 0) {
|
||||
clearInterval(interval)
|
||||
this.smsBtnText = this.$t('SendVerificationCode')
|
||||
this.smsBtnDisabled = false
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$tc('FailedToSendVerificationCode'))
|
||||
})
|
||||
},
|
||||
handlePasskeyVerify() {
|
||||
this.passkeyVisible = true
|
||||
this.checkPasskeyStatus()
|
||||
},
|
||||
checkPasskeyStatus() {
|
||||
const url = '/api/v1/authentication/confirm/check/?confirm_type=mfa'
|
||||
const t = setInterval(() => {
|
||||
this.$axios.get(url).then(data => {
|
||||
this.passkeyVisible = false
|
||||
this.onSuccess()
|
||||
})
|
||||
}, 2000)
|
||||
setTimeout(() => {
|
||||
clearInterval(t)
|
||||
if (this.passkeyVisible) {
|
||||
this.passkeyVisible = false
|
||||
this.$message.error(this.$tc('PasskeyTimeout'))
|
||||
}
|
||||
}, 20000)
|
||||
},
|
||||
startFaceCapture() {
|
||||
const url = '/api/v1/authentication/face/context/'
|
||||
this.$axios.post(url).then(data => {
|
||||
const token = data['token']
|
||||
this.faceCaptureUrl = '/facelive/capture?token=' + token
|
||||
this.isFaceCaptureVisible = true
|
||||
this.$axios
|
||||
.post(url)
|
||||
.then(data => {
|
||||
const token = data['token']
|
||||
this.faceCaptureUrl = '/facelive/capture?token=' + token
|
||||
this.isFaceCaptureVisible = true
|
||||
|
||||
const timer = setInterval(() => {
|
||||
this.$axios.get(url + `?token=${token}`).then(data => {
|
||||
if (data['is_finished']) {
|
||||
clearInterval(timer)
|
||||
this.isFaceCaptureVisible = false
|
||||
this.handleConfirm()
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$tc('FailedToStartFaceCapture'))
|
||||
})
|
||||
const timer = setInterval(() => {
|
||||
this.$axios.get(url + `?token=${token}`).then(data => {
|
||||
if (data['is_finished']) {
|
||||
clearInterval(timer)
|
||||
this.isFaceCaptureVisible = false
|
||||
this.handleConfirm()
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$tc('FailedToStartFaceCapture'))
|
||||
})
|
||||
},
|
||||
handleFaceCapture() {
|
||||
this.startFaceCapture()
|
||||
},
|
||||
onSuccess() {
|
||||
this.secretValue = ''
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.callback()
|
||||
})
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
return this.logout()
|
||||
@@ -258,20 +313,22 @@ export default {
|
||||
const data = {
|
||||
confirm_type: this.confirmTypeRequired,
|
||||
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
|
||||
secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue
|
||||
secret_key:
|
||||
this.confirmTypeRequired === 'password'
|
||||
? encryptPassword(this.secretValue)
|
||||
: this.secretValue
|
||||
}
|
||||
|
||||
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(() => {
|
||||
this.secretValue = ''
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.callback()
|
||||
this.$axios
|
||||
.post(`/api/v1/authentication/confirm/`, data)
|
||||
.then(() => {
|
||||
this.onSuccess()
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error(err.message || this.$tc('ConfirmFailed'))
|
||||
this.faceCaptureUrl = null
|
||||
this.isFaceCaptureVisible = false
|
||||
})
|
||||
}).catch((err) => {
|
||||
this.$message.error(err.message || this.$tc('ConfirmFailed'))
|
||||
this.faceCaptureUrl = null
|
||||
this.isFaceCaptureVisible = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
['', ['name', 'var_name', 'type', 'text_default_value', 'select_default_value', 'extra_args', 'tips', 'required']]
|
||||
],
|
||||
fieldsMeta: {
|
||||
var_name: {
|
||||
helpTextAsTip: false
|
||||
},
|
||||
text_default_value: {
|
||||
label: this.$t('DefaultValue'),
|
||||
hidden: (formValue) => {
|
||||
|
||||
100
src/components/Apps/VariablesHelpTextDialog/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="iVisible"
|
||||
class="help-dialog"
|
||||
top="1vh"
|
||||
width="50%"
|
||||
>
|
||||
<p>{{ variablesHelpText }}</p>
|
||||
<table border="1" class="help-table">
|
||||
<tr>
|
||||
<th>{{ $tc('Variable') }}</th>
|
||||
<th>{{ $tc('Description') }}</th>
|
||||
<th>{{ $tc('Example') }}</th>
|
||||
</tr>
|
||||
<tr v-for="(item, index) in variables" :key="index">
|
||||
<td :title="$tc('ClickCopy')" class="item-td text-link" @click="onCopy(item.name)">
|
||||
<label class="item-label">{{ item.name }}</label>
|
||||
</td>
|
||||
<td><span>{{ item.label }}</span></td>
|
||||
<td><span>{{ item.default }}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { copy } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
variables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
variablesHelpText: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('WatermarkVariableHelpText')
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: this.$t('BuiltinVariable')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
},
|
||||
get() {
|
||||
return this.visible
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCopy(key) {
|
||||
copy(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .help-dialog.dialog .el-dialog__footer {
|
||||
border-top: none;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.help-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
::v-deep .help-table th,
|
||||
::v-deep .help-table td {
|
||||
height: 40px;
|
||||
padding: 0 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
::v-deep .help-table .item-td,
|
||||
::v-deep .help-table .item-label {
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script type="text/jsx">
|
||||
import { toSafeLocalDateStr } from '@/utils/time'
|
||||
import { toSafeLocalDateStr } from '@/utils/common/time'
|
||||
|
||||
export default {
|
||||
name: 'ItemValue',
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<script>
|
||||
import DetailCard from './index.vue'
|
||||
import { copy } from '@/utils/common'
|
||||
import { toSafeLocalDateStr } from '@/utils/time'
|
||||
import { copy } from '@/utils/common/index'
|
||||
import { toSafeLocalDateStr } from '@/utils/common/time'
|
||||
import IBox from '@/components/Common/IBox/index.vue'
|
||||
import LabelsDetailFormatter from '../Formatters/LabelsDetailFormatter.vue'
|
||||
|
||||
@@ -129,8 +129,18 @@ export default {
|
||||
parseArrayValue(value, excludes, label) {
|
||||
if (Array.isArray(value)) {
|
||||
const tp = typeof value[0]
|
||||
let object = {}
|
||||
|
||||
if (value.length === 0) {
|
||||
object = {
|
||||
key: label,
|
||||
value: '-'
|
||||
}
|
||||
return this.items.push(object)
|
||||
}
|
||||
|
||||
// 如果是空数组,那么循环体将不会执行
|
||||
for (const [index, item] of value.entries()) {
|
||||
let object = {}
|
||||
if (tp === 'object') {
|
||||
const firstValue = value[0]
|
||||
if (firstValue.hasOwnProperty('name')) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div v-if="item.has !== false" :key="item.key" :class="item.class " :label="item.key" class="el-form-item">
|
||||
<span slot="label" class="el-form-item__label"> {{ formateLabel(item.key) }}</span>
|
||||
<span class="item-value el-form-item__content">
|
||||
<template
|
||||
<component
|
||||
:is="item.component"
|
||||
v-if="item.component"
|
||||
v-bind="{...item}"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-if="action.dropdown"
|
||||
v-show="action.dropdown.length > 0"
|
||||
:key="action.name"
|
||||
:class="[action.name, {grouped: action.grouped }]"
|
||||
:class="[action.name, { grouped: action.grouped }]"
|
||||
:size="action.size"
|
||||
:split-button="!!action.split"
|
||||
:type="action.type"
|
||||
@@ -15,7 +15,7 @@
|
||||
@click="handleClick(action)"
|
||||
@command="handleDropdownCallback"
|
||||
>
|
||||
<span v-if="action.split">
|
||||
<span v-if="action.split" :style="{ cursor: action.disabled ? 'not-allowed' : 'pointer' }">
|
||||
{{ action.title }}
|
||||
</span>
|
||||
<el-button
|
||||
@@ -23,7 +23,7 @@
|
||||
:class="action.name"
|
||||
:size="size"
|
||||
class="more-action"
|
||||
v-bind="{...cleanButtonAction(action), icon: ''}"
|
||||
v-bind="{ ...cleanButtonAction(action), icon: '' }"
|
||||
>
|
||||
<span class="pre-icon">
|
||||
<Icon v-if="action.icon" :icon="action.icon" />
|
||||
@@ -32,13 +32,13 @@
|
||||
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
|
||||
</span>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown" style="overflow: auto;max-height: 60vh">
|
||||
<el-dropdown-menu slot="dropdown" style="overflow: auto; max-height: 60vh">
|
||||
<template v-for="option in action.dropdown">
|
||||
<div
|
||||
v-if="option.group"
|
||||
:key="'group:'+option.name"
|
||||
:key="'group:' + option.name"
|
||||
class="dropdown-menu-title"
|
||||
style="width:130px"
|
||||
style="width: 130px"
|
||||
>
|
||||
{{ option.group }}
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
:command="[option, action]"
|
||||
:title="option.tip"
|
||||
class="dropdown-item"
|
||||
v-bind="{...option, icon: ''}"
|
||||
v-bind="{ ...option, icon: '' }"
|
||||
>
|
||||
<span v-if="actionsHasIcon(action.dropdown)" class="pre-icon">
|
||||
<Icon v-if="option.icon" :icon="option.icon" />
|
||||
@@ -69,10 +69,10 @@
|
||||
<el-button
|
||||
v-else
|
||||
:key="action.name"
|
||||
:class="[action.name, {grouped: action.grouped }]"
|
||||
:class="[action.name, { grouped: action.grouped }]"
|
||||
:size="size"
|
||||
class="action-item"
|
||||
v-bind="{...cleanButtonAction(action), icon: ''}"
|
||||
v-bind="{ ...cleanButtonAction(action), icon: '' }"
|
||||
@click="handleClick(action)"
|
||||
>
|
||||
<el-tooltip :content="action.tip" :disabled="!action.tip" placement="top">
|
||||
@@ -89,7 +89,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toSentenceCase } from '@/utils/common'
|
||||
import { toSentenceCase } from '@/utils/common/index'
|
||||
import Icon from '@/components/Widgets/Icon/index.vue'
|
||||
|
||||
export default {
|
||||
@@ -228,9 +228,9 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$btn-text-color: #ffffff;
|
||||
$color-btn-background: #E8F7F4;
|
||||
$color-btn-focus-background: #83CBBA;
|
||||
$color-divided: #E4E7ED;
|
||||
$color-btn-background: #e8f7f4;
|
||||
$color-btn-focus-background: #83cbba;
|
||||
$color-divided: #e4e7ed;
|
||||
$color-drop-menu-title: #909399;
|
||||
$color-drop-menu-border: #e4e7ed;
|
||||
|
||||
@@ -284,6 +284,8 @@ $color-drop-menu-border: #e4e7ed;
|
||||
|
||||
.el-button {
|
||||
padding: 2px 5px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
|
||||
&:not(.is-plain) {
|
||||
color: $btn-text-color;
|
||||
@@ -319,7 +321,6 @@ $color-drop-menu-border: #e4e7ed;
|
||||
// 下拉 options
|
||||
.el-dropdown-menu {
|
||||
::v-deep .more-batch-processing {
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td>{{ getActionTitle(action) }}</td>
|
||||
<td>
|
||||
<el-popover
|
||||
:content="action.attrs.tip"
|
||||
:disabled="!action.attrs.showTip"
|
||||
placement="left-end"
|
||||
trigger="hover"
|
||||
>
|
||||
<span slot="reference">
|
||||
<component
|
||||
:is="iType"
|
||||
v-model="action.attrs.model"
|
||||
:title="label"
|
||||
v-bind="action.attrs"
|
||||
v-on="callbacks"
|
||||
>
|
||||
{{ label }}
|
||||
</component>
|
||||
</span>
|
||||
</el-popover>
|
||||
{{ getActionTitle(action) }}
|
||||
<el-tooltip v-if="action.attrs.showTip" :content="action.attrs.tip" :open-delay="500" effect="dark">
|
||||
<i class="fa fa-question-circle-o" />
|
||||
</el-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<span slot="reference">
|
||||
<component
|
||||
:is="iType"
|
||||
v-model="action.attrs.model"
|
||||
:title="label"
|
||||
v-bind="action.attrs"
|
||||
v-on="callbacks"
|
||||
>
|
||||
{{ label }}
|
||||
</component>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -28,7 +26,7 @@
|
||||
import Switcher from '@/components/Form/FormFields/Switcher.vue'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import UpdateSelect from '@/components/Form/FormFields/UpdateSelect.vue'
|
||||
import { toSentenceCase } from '@/utils/common'
|
||||
import { toSentenceCase } from '@/utils/common/index'
|
||||
|
||||
class Action {
|
||||
constructor() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<echarts
|
||||
<div>
|
||||
<Echart
|
||||
ref="echarts"
|
||||
:options="options"
|
||||
:autoresize="true"
|
||||
@@ -12,9 +12,10 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import * as echarts from 'echarts'
|
||||
import { mix } from '@/utils/theme/color'
|
||||
import Echart from '@/components/Dashboard/Echart.vue'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
components: { Echart },
|
||||
props: {
|
||||
datesMetrics: {
|
||||
type: Array,
|
||||
|
||||
71
src/components/Dashboard/Echart.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<echarts
|
||||
:options="iOptions"
|
||||
v-bind="$attrs"
|
||||
@finished="onFinished"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'echarts'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const isExport = urlParams.get('export') === 'true'
|
||||
return {
|
||||
isExport: isExport
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iOptions() {
|
||||
return {
|
||||
...this.options,
|
||||
animation: !this.isExport
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!window._echarts) {
|
||||
window._echarts = {
|
||||
total: new Set(),
|
||||
finished: new Set()
|
||||
}
|
||||
}
|
||||
// 唯一 id,避免重复计数
|
||||
this._chartId = `chart_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
||||
window._echarts.total.add(this._chartId)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (window._echarts) {
|
||||
window._echarts.total.delete(this._chartId)
|
||||
window._echarts.finished.delete(this._chartId)
|
||||
// 可选:当没有图表时清理全局对象
|
||||
if (window._echarts.total.size === 0) {
|
||||
delete window._echarts
|
||||
delete window.echartsFinished
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onFinished() {
|
||||
if (!window._echarts) return
|
||||
window._echarts.finished.add(this._chartId)
|
||||
if (window._echarts.finished.size === window._echarts.total.size) {
|
||||
window.echartsFinished = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<echarts
|
||||
<Echart
|
||||
ref="echarts"
|
||||
:options="options"
|
||||
:autoresize="true"
|
||||
theme="light"
|
||||
class="disabled-when-print"
|
||||
@finished="getDataUrl"
|
||||
@finished="genSnapshot"
|
||||
/>
|
||||
<img v-if="dataUrl" :src="dataUrl" class="enabled-when-print" style="display: none;width: 100%;">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,9 +14,11 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import * as echarts from 'echarts'
|
||||
import { mix } from '@/utils/theme/color'
|
||||
import Echart from '@/components/Dashboard/Echart.vue'
|
||||
|
||||
export default {
|
||||
name: 'LoginMetric',
|
||||
components: { Echart },
|
||||
props: {
|
||||
range: {
|
||||
type: String,
|
||||
@@ -110,7 +110,6 @@ export default {
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
// 坐标轴颜色
|
||||
color: '#8F959E'
|
||||
}
|
||||
},
|
||||
@@ -155,7 +154,7 @@ export default {
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
// 区域填充样式
|
||||
// 区域填充样式
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(
|
||||
0,
|
||||
@@ -187,7 +186,7 @@ export default {
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
// 区域填充样式
|
||||
// 区域填充样式
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(
|
||||
0,
|
||||
@@ -220,23 +219,58 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
range() {
|
||||
this.getMetricData()
|
||||
this.genSnapshot()
|
||||
},
|
||||
datesMetrics() {
|
||||
this.genSnapshot()
|
||||
},
|
||||
primaryData() {
|
||||
this.genSnapshot()
|
||||
},
|
||||
secondaryData() {
|
||||
this.genSnapshot()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMetricData()
|
||||
this.genSnapshot()
|
||||
this._before = () => this.genSnapshot(true)
|
||||
this._after = () => this.forceResize()
|
||||
window.addEventListener('beforeprint', this._before)
|
||||
window.addEventListener('afterprint', this._after)
|
||||
// 兼容某些浏览器(Safari)触发 print 媒体切换
|
||||
this._mql = window.matchMedia && window.matchMedia('print')
|
||||
if (this._mql) {
|
||||
const handler = e => (e.matches ? this._before() : this._after())
|
||||
this._mql.addEventListener?.('change', handler)
|
||||
this._mql.addListener?.(handler)
|
||||
this._mql._handler = handler
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeprint', this._before)
|
||||
window.removeEventListener('afterprint', this._after)
|
||||
if (this._mql) {
|
||||
this._mql.removeEventListener?.('change', this._mql._handler)
|
||||
this._mql.removeListener?.(this._mql._handler)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDataUrl() {
|
||||
const instance = this.$refs.echarts.echartsInstance
|
||||
if (instance) {
|
||||
this.dataUrl = instance.getDataURL()
|
||||
}
|
||||
forceResize() {
|
||||
const inst = this.$refs.echarts?.echartsInstance
|
||||
if (inst) inst.resize()
|
||||
},
|
||||
getMetricData() {
|
||||
this.getDataUrl()
|
||||
async genSnapshot(force = false) {
|
||||
if (force) this.forceResize()
|
||||
const inst = this.$refs.echarts?.echartsInstance
|
||||
if (!inst) return
|
||||
try {
|
||||
this.dataUrl = inst.getDataURL({ pixelRatio: 2, backgroundColor: '#ffffff' })
|
||||
} catch (e) {
|
||||
this.dataUrl = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -245,16 +279,4 @@ export default {
|
||||
width: 100%;
|
||||
height: 272px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.disabled-when-print {
|
||||
display: none;
|
||||
}
|
||||
.enabled-when-print {
|
||||
display: inherit !important;
|
||||
}
|
||||
.print-margin {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<echarts
|
||||
<Echart
|
||||
ref="echarts"
|
||||
:options="options"
|
||||
:autoresize="true"
|
||||
@@ -13,8 +13,10 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import * as echarts from 'echarts'
|
||||
import { mix } from '@/utils/theme/color'
|
||||
import Echart from '@/components/Dashboard/Echart.vue'
|
||||
|
||||
export default {
|
||||
components: { Echart },
|
||||
props: {
|
||||
colors: {
|
||||
type: Array,
|
||||
@@ -35,14 +37,15 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
const seriesList = []
|
||||
const labels = this.data.map(item => item.label)
|
||||
const total = _.sumBy(this.data, function(i) { return i.total })
|
||||
const total = _.sumBy(this.data, function(i) {
|
||||
return i.total
|
||||
})
|
||||
for (let i = 0, len = this.data.length; i < len; i++) {
|
||||
const current = this.data[i]
|
||||
let num = (current.total / total) * 100
|
||||
@@ -177,8 +180,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class="table"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column :label="$tc('Ranking')">
|
||||
<el-table-column :label="$tc('Ranking')" width="100">
|
||||
<template #header>
|
||||
<el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500">
|
||||
<span style="cursor: pointer;">{{ $t('Ranking') }}</span>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
{
|
||||
title: this.$t('OnlineSessions'),
|
||||
body: {
|
||||
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' }},
|
||||
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' } },
|
||||
count: this.counter.total_count_online_sessions,
|
||||
disabled: !this.$hasPerm('terminal.view_session')
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
{
|
||||
title: this.$t('CurrentConnectionUsers'),
|
||||
body: {
|
||||
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' }},
|
||||
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' } },
|
||||
count: this.counter.total_count_online_users,
|
||||
disabled: !this.$hasPerm('terminal.view_session')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<echarts
|
||||
<Echart
|
||||
ref="echarts"
|
||||
:autoresize="true"
|
||||
:options="options"
|
||||
@@ -13,12 +13,15 @@ import 'echarts/lib/chart/line'
|
||||
import 'echarts/lib/component/legend'
|
||||
|
||||
import Decimal from 'decimal.js'
|
||||
import Echart from '@/components/Dashboard/Echart.vue'
|
||||
|
||||
export default {
|
||||
components: { Echart },
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
default: () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -33,7 +36,7 @@ export default {
|
||||
|
||||
const formatTitle = (text) => {
|
||||
if (!text) return ''
|
||||
const maxLength = 23
|
||||
const maxLength = 25
|
||||
const lines = []
|
||||
for (let i = 0; i < text.length; i += maxLength) {
|
||||
lines.push(text.slice(i, i + maxLength))
|
||||
@@ -88,6 +91,7 @@ export default {
|
||||
{
|
||||
name: title,
|
||||
type: 'pie',
|
||||
minAngle: 5,
|
||||
radius: ['72%', '90%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
days: {
|
||||
name: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return localStorage.getItem('dashboardDays') || '7'
|
||||
}
|
||||
default: 'dashboardDays'
|
||||
},
|
||||
days: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
@@ -47,8 +49,28 @@ export default {
|
||||
iOptions: this.options.length > 0 ? this.options : defaultOptions
|
||||
}
|
||||
},
|
||||
created() {
|
||||
let days = this.days
|
||||
if (!days) {
|
||||
days = this.$route.query.days
|
||||
}
|
||||
if (!days) {
|
||||
days = localStorage.getItem(this.name)
|
||||
}
|
||||
if (!days) {
|
||||
days = '7'
|
||||
}
|
||||
if (days && days !== this.select) {
|
||||
this.select = days
|
||||
this.$emit('change', days)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('change', this.select)
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
localStorage.setItem(this.name, val)
|
||||
this.$emit('change', val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<script>
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { copy } from '@/utils/common'
|
||||
import { copy } from '@/utils/common/index'
|
||||
import Dialog from '@/components/Dialog/index'
|
||||
|
||||
export default {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDrawerWidth } from '@/utils/common'
|
||||
import { getDrawerWidth } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
ref="dataForm"
|
||||
:fields="totalFields"
|
||||
:form="iForm"
|
||||
:server-errors="serverErrors"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
@@ -28,6 +29,7 @@
|
||||
import DataForm from '../DataForm/index.vue'
|
||||
import FormGroupHeader from '@/components/Form/FormGroupHeader/index.vue'
|
||||
import { FormFieldGenerator } from '@/components/Form/AutoDataForm/utils'
|
||||
import { UniqueCheck } from '@/components/Form/DataForm/rules'
|
||||
|
||||
export default {
|
||||
name: 'AutoDataForm',
|
||||
@@ -65,7 +67,8 @@ export default {
|
||||
totalFields: [],
|
||||
loading: true,
|
||||
groups: [],
|
||||
errors: {}
|
||||
errors: {},
|
||||
serverErrors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -94,7 +97,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async optionUrlMetaAndGenerateColumns() {
|
||||
let data = { actions: {}}
|
||||
let data = { actions: {} }
|
||||
if (this.url) {
|
||||
data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
|
||||
}
|
||||
@@ -103,6 +106,8 @@ export default {
|
||||
this.generateColumns()
|
||||
this.$emit('afterGenerateColumns', this.totalFields)
|
||||
this.cleanFormValue()
|
||||
// 初始化时清空错误
|
||||
this.serverErrors = {}
|
||||
this.loading = false
|
||||
},
|
||||
generateColumns() {
|
||||
@@ -110,6 +115,47 @@ export default {
|
||||
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
|
||||
this.groups = generator.groups
|
||||
this.$log.debug('Total fields: ', this.totalFields)
|
||||
this.applyUniqueRules()
|
||||
},
|
||||
applyUniqueRules() {
|
||||
const fields = this.totalFields || []
|
||||
const currentIdGetter = () => {
|
||||
return this.$route?.params?.id || this.form?.id || this.iForm?.id
|
||||
}
|
||||
|
||||
// 移除 url 后拼接的参数
|
||||
const defaultListUrl = (() => {
|
||||
try {
|
||||
const u = new URL(this.url, location.origin)
|
||||
u.pathname = u.pathname.replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?$/, '/')
|
||||
return u.origin ? u.origin + u.pathname : u.pathname
|
||||
} catch (e) {
|
||||
return (this.url || '').replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?($|\?)/, '/$2')
|
||||
}
|
||||
})()
|
||||
|
||||
fields.forEach(field => {
|
||||
const conf = field?.uniqueCheck
|
||||
|
||||
if (!conf) return
|
||||
|
||||
const confObj = (typeof conf === 'object') ? conf : {}
|
||||
const param = confObj.param || field.prop || field.id
|
||||
const url = confObj.url || defaultListUrl
|
||||
const label = confObj.label || field.label || param
|
||||
const entityName = confObj.entityName || ''
|
||||
|
||||
if (!Array.isArray(field.rules)) field.rules = []
|
||||
|
||||
field.rules.push(UniqueCheck({
|
||||
url,
|
||||
param,
|
||||
label,
|
||||
entityName,
|
||||
getIgnoreId: currentIdGetter,
|
||||
fieldName: field.prop || field.id
|
||||
}))
|
||||
})
|
||||
},
|
||||
_cleanFormValue(form, remoteMeta) {
|
||||
if (!form) {
|
||||
@@ -139,18 +185,69 @@ export default {
|
||||
cleanFormValue() {
|
||||
this._cleanFormValue(this.iForm, this.remoteMeta)
|
||||
},
|
||||
_getElFormInstance() {
|
||||
try {
|
||||
return this.$refs?.dataForm?.$refs?.form?.$refs?.elForm || null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 仅清理 UI 的错误展示,不触发表单内容重建
|
||||
*/
|
||||
clearAllFieldErrors() {
|
||||
const elForm = this._getElFormInstance()
|
||||
if (elForm && Array.isArray(elForm.fields)) {
|
||||
elForm.fields.forEach((item) => {
|
||||
item.validateMessage = ''
|
||||
item.validateState = ''
|
||||
})
|
||||
}
|
||||
// 不修改 totalFields/attrs,避免触发 content 重建导致输入丢失
|
||||
this.serverErrors = {}
|
||||
},
|
||||
setFieldError(name, error) {
|
||||
const field = this.totalFields.find((v) => v.prop === name)
|
||||
if (!field) {
|
||||
return
|
||||
error = (error || '').toString().replace(/[。.]+$/, '')
|
||||
const elForm = this._getElFormInstance()
|
||||
if (elForm && Array.isArray(elForm.fields)) {
|
||||
const item = elForm.fields.find(f => f.prop === name)
|
||||
if (item) {
|
||||
item.validateMessage = error
|
||||
item.validateState = error ? 'error' : ''
|
||||
}
|
||||
}
|
||||
if (field.attrs.error === error) {
|
||||
error += '.'
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
field.el.errors = error
|
||||
field.attrs.error = error
|
||||
// 不写入 totalFields,避免触发 innerContent 变化导致表单值被覆盖
|
||||
this.$set(this.serverErrors, name, error)
|
||||
},
|
||||
setErrors(errors) {
|
||||
const mapped = {}
|
||||
Object.entries(errors || {}).forEach(([k, v]) => {
|
||||
let msg = v
|
||||
console.log(k, v)
|
||||
// v是数组并且数组都是字符串,则拼接为字符串
|
||||
if (Array.isArray(v) && v.every(item => typeof item === 'string')) msg = v.join('; ')
|
||||
// 处理 [{"port":["请确保该值小于或者等于 65535。"]},{},{}] 这种情况
|
||||
else if (Array.isArray(v) && v.every(item => _.isPlainObject(item))) {
|
||||
const subMsg = []
|
||||
v.forEach((subItem) => {
|
||||
Object.values(subItem).forEach((subMsgArr) => {
|
||||
if (Array.isArray(subMsgArr)) {
|
||||
subMsg.push(...subMsgArr)
|
||||
}
|
||||
})
|
||||
})
|
||||
msg = subMsg.join(' ')
|
||||
} else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
|
||||
mapped[k] = String(msg || '')
|
||||
})
|
||||
this.serverErrors = mapped
|
||||
const elForm = this._getElFormInstance()
|
||||
if (elForm && Array.isArray(elForm.fields)) {
|
||||
elForm.fields.forEach((item) => {
|
||||
const msg = mapped[item.prop] || ''
|
||||
item.validateMessage = msg
|
||||
item.validateState = msg ? 'error' : ''
|
||||
})
|
||||
}
|
||||
},
|
||||
groupHidden(group, i) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import NestedField from '@/components/Form/AutoDataForm/components/NestedField.v
|
||||
import rules from '@/components/Form/DataForm/rules'
|
||||
import BasicTree from '@/components/Form/FormFields/BasicTree.vue'
|
||||
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
|
||||
import { assignIfNot, toSentenceCase } from '@/utils/common'
|
||||
import { assignIfNot, toSentenceCase } from '@/utils/common/index'
|
||||
import TagInput from '@/components/Form/FormFields/TagInput.vue'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
@@ -177,7 +177,10 @@ export class FormFieldGenerator {
|
||||
const systemLang = document.cookie.django_language
|
||||
if (helpTextAsPlaceholder !== undefined) {
|
||||
helpTextAsPlaceholder = !!helpTextAsPlaceholder
|
||||
} else if (placeholderType.indexOf(field.type) === -1 && placeholderComponent.indexOf(field.component) === -1) {
|
||||
} else if (
|
||||
placeholderType.indexOf(field.type) === -1 &&
|
||||
placeholderComponent.indexOf(field.component) === -1
|
||||
) {
|
||||
helpTextAsPlaceholder = false
|
||||
} else if ((helpTextWordLength <= 5 || helpText.length <= 10) && systemLang === 'en') {
|
||||
helpTextAsPlaceholder = true
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:prop="prop"
|
||||
:rules="_show && Array.isArray(data.rules) ? data.rules : []"
|
||||
v-bind="data.attrs"
|
||||
:error="errorText"
|
||||
>
|
||||
<template v-if="data.label" #label>
|
||||
<span :title="data.label">
|
||||
@@ -19,7 +20,8 @@
|
||||
placement="right"
|
||||
popper-class="help-tips"
|
||||
>
|
||||
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" /> <!-- Noncompliant -->
|
||||
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" />
|
||||
<!-- Noncompliant -->
|
||||
<i class="fa fa-question-circle-o help-tip-icon" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
@@ -27,11 +29,7 @@
|
||||
<template v-if="readonly && hasReadonlyContent">
|
||||
<div
|
||||
v-if="data.type === 'input'"
|
||||
:style="
|
||||
componentProps.type === 'textarea'
|
||||
? {padding: '10px 0', lineHeight: 1.5}
|
||||
: ''
|
||||
"
|
||||
:style="componentProps.type === 'textarea' ? { padding: '10px 0', lineHeight: 1.5 } : ''"
|
||||
>
|
||||
{{ itemValue }}
|
||||
</div>
|
||||
@@ -50,11 +48,7 @@
|
||||
v-on="listeners"
|
||||
>
|
||||
<template v-for="opt in options">
|
||||
<el-option
|
||||
v-if="data.type === 'select'"
|
||||
:key="opt.label"
|
||||
v-bind="opt"
|
||||
/>
|
||||
<el-option v-if="data.type === 'select'" :key="opt.label" v-bind="opt" />
|
||||
<el-checkbox-button
|
||||
v-else-if="data.type === 'checkbox-group' && data.style === 'button'"
|
||||
:key="opt.value"
|
||||
@@ -111,10 +105,10 @@
|
||||
<script>
|
||||
import getEnableWhenStatus from '../util/enable-when'
|
||||
import { noop } from '../util/utils'
|
||||
import _get from 'lodash.get'
|
||||
import _includes from 'lodash.includes'
|
||||
import _topairs from 'lodash.topairs'
|
||||
import _frompairs from 'lodash.frompairs'
|
||||
import _get from 'lodash/get'
|
||||
import _includes from 'lodash/includes'
|
||||
import _topairs from 'lodash/toPairs'
|
||||
import _frompairs from 'lodash/fromPairs'
|
||||
|
||||
function validator(data) {
|
||||
if (!data) {
|
||||
@@ -152,6 +146,10 @@ export default {
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
data: Object,
|
||||
serverErrors: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
prop: {
|
||||
type: String,
|
||||
default() {
|
||||
@@ -170,7 +168,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
propsInner: {},
|
||||
isBlurTrigger: this.data.rules &&
|
||||
isBlurTrigger:
|
||||
this.data.rules &&
|
||||
this.data.rules.some(rule => {
|
||||
return rule.required && rule.trigger === 'blur'
|
||||
})
|
||||
@@ -179,7 +178,7 @@ export default {
|
||||
computed: {
|
||||
// 解构运算符会处理 undefined 的情况
|
||||
componentProps: ({ data: { el }, propsInner }) => ({ ...el, ...propsInner }),
|
||||
hasReadonlyContent: ({ data: { type }}) => _includes(['input', 'select'], type),
|
||||
hasReadonlyContent: ({ data: { type } }) => _includes(['input', 'select'], type),
|
||||
hiddenStatus: ({ data: { hidden = () => false }, data, value }) => hidden(value, data),
|
||||
enableWhenStatus: ({ data: { enableWhen }, value }) => getEnableWhenStatus(enableWhen, value),
|
||||
// 是否显示
|
||||
@@ -189,6 +188,11 @@ export default {
|
||||
classes() {
|
||||
return 'el-form-item-' + this.data.prop + ' ' + (this.data.attrs?.class || '')
|
||||
},
|
||||
errorText() {
|
||||
const fromAttrs = this.data?.attrs?.error
|
||||
const fromServer = this.serverErrors ? this.serverErrors[this.data.prop] : ''
|
||||
return fromAttrs || fromServer || ''
|
||||
},
|
||||
listeners() {
|
||||
const {
|
||||
data: {
|
||||
@@ -204,10 +208,7 @@ export default {
|
||||
} = this
|
||||
return {
|
||||
..._frompairs(
|
||||
_topairs(on).map(([eName, handler]) => [
|
||||
eName,
|
||||
(...args) => handler(args, updateForm)
|
||||
]),
|
||||
_topairs(on).map(([eName, handler]) => [eName, (...args) => handler(args, updateForm)])
|
||||
),
|
||||
// 手动更新表单数据
|
||||
input: (value, ...rest) => {
|
||||
@@ -232,9 +233,7 @@ export default {
|
||||
|
||||
multipleValue: ({ data, itemValue, options = [] }) => {
|
||||
const multipleSelectValue =
|
||||
_get(data, 'el.multiple') && Array.isArray(itemValue)
|
||||
? itemValue
|
||||
: [itemValue]
|
||||
_get(data, 'el.multiple') && Array.isArray(itemValue) ? itemValue : [itemValue]
|
||||
return multipleSelectValue
|
||||
.map(val => (options.find(op => op.value === val) || {}).label)
|
||||
.join()
|
||||
@@ -254,8 +253,7 @@ export default {
|
||||
if (v.url === oldV.url || v.request === oldV.request) return
|
||||
}
|
||||
const isOptionsCase =
|
||||
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) >
|
||||
-1
|
||||
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) > -1
|
||||
const {
|
||||
url,
|
||||
request = () => this.$axios.get(url).then(resp => resp.data),
|
||||
@@ -323,7 +321,7 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.help-tips {
|
||||
opacity: 0.8;
|
||||
line-height: 2;
|
||||
@@ -332,7 +330,7 @@ export default {
|
||||
|
||||
.help-block {
|
||||
::v-deep .el-alert__icon {
|
||||
font-size: 16px
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.checkbox {
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<template>
|
||||
<el-form ref="elForm" :model="value" class="el-form-renderer" v-bind="$attrs">
|
||||
<el-form
|
||||
ref="elForm"
|
||||
:model="value"
|
||||
class="el-form-renderer"
|
||||
v-bind="$attrs"
|
||||
@submit.native.prevent
|
||||
>
|
||||
<template v-for="item in innerContent">
|
||||
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
|
||||
<component
|
||||
:is="item.type === GROUP ? 'render-form-group' : 'render-form-item'"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
:server-errors="serverErrors"
|
||||
:disabled="disabled || item.disabled"
|
||||
:item-value="value[item.id]"
|
||||
:options="options[item.id]"
|
||||
@@ -19,13 +26,19 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script>
|
||||
import _set from 'lodash.set'
|
||||
import _isequal from 'lodash.isequal'
|
||||
import _clonedeep from 'lodash.clonedeep'
|
||||
import _set from 'lodash/set'
|
||||
import _isequal from 'lodash/isEqual'
|
||||
import _clonedeep from 'lodash/cloneDeep'
|
||||
import RenderFormGroup from './components/render-form-group.vue'
|
||||
import RenderFormItem from './components/render-form-item.vue'
|
||||
import transformContent from './util/transform-content'
|
||||
import { collect, correctValue, mergeValue, transformInputValue, transformOutputValue } from './util/utils'
|
||||
import {
|
||||
collect,
|
||||
correctValue,
|
||||
mergeValue,
|
||||
transformInputValue,
|
||||
transformOutputValue
|
||||
} from './util/utils'
|
||||
|
||||
const GROUP = 'group'
|
||||
|
||||
@@ -47,6 +60,10 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
serverErrors: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _get from 'lodash.get'
|
||||
import _has from 'lodash.has'
|
||||
import _get from 'lodash/get'
|
||||
import _has from 'lodash/has'
|
||||
|
||||
/**
|
||||
* 处理 enableWhen
|
||||
@@ -20,7 +20,5 @@ export default function getEnableWhenStatus(enableWhen, value) {
|
||||
})
|
||||
}
|
||||
|
||||
return Array.isArray(enableWhen)
|
||||
? enableWhen.some(handleCondition)
|
||||
: handleCondition(enableWhen)
|
||||
return Array.isArray(enableWhen) ? enableWhen.some(handleCondition) : handleCondition(enableWhen)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-sequences */
|
||||
import _kebabcase from 'lodash.kebabcase'
|
||||
import _ from 'lodash'
|
||||
/**
|
||||
* content 的每一项会浅拷贝一层
|
||||
* 只可以在 item 层新增修改属性,如 item.a = b
|
||||
@@ -13,7 +13,7 @@ export default function transformContent(content) {
|
||||
removeDollarInKey(item)
|
||||
extractRulesFromComponent(item)
|
||||
// 有些旧写法是 checkboxGroup & radioGroup
|
||||
item.type = _kebabcase(item.type)
|
||||
item.type = _.kebabCase(item.type)
|
||||
}
|
||||
|
||||
return item
|
||||
@@ -34,8 +34,5 @@ export function extractRulesFromComponent(item) {
|
||||
if (!component || typeof component === 'string') return
|
||||
|
||||
const { rules = [] } = component
|
||||
item.rules = [
|
||||
...(item.rules || []),
|
||||
...(typeof rules === 'function' ? rules(item) : rules)
|
||||
]
|
||||
item.rules = [...(item.rules || []), ...(typeof rules === 'function' ? rules(item) : rules)]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _frompairs from 'lodash.frompairs'
|
||||
import _isplainobject from 'lodash.isplainobject'
|
||||
import _frompairs from 'lodash/fromPairs'
|
||||
import _isplainobject from 'lodash/isPlainObject'
|
||||
|
||||
export function noop() {}
|
||||
|
||||
@@ -12,11 +12,9 @@ export function collect(content, key) {
|
||||
value: item.type === 'group' ? collect(item.items, key) : item[key]
|
||||
}))
|
||||
.filter(
|
||||
({ type, value }) =>
|
||||
value !== undefined ||
|
||||
(type === 'group' && Object.keys(value).length),
|
||||
({ type, value }) => value !== undefined || (type === 'group' && Object.keys(value).length)
|
||||
)
|
||||
.map(({ id, value }) => [id, value]),
|
||||
.map(({ id, value }) => [id, value])
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
:form="basicForm"
|
||||
:label-position="iLabelPosition"
|
||||
class="form-fields"
|
||||
label-width="25%"
|
||||
:label-width="labelWidth"
|
||||
:style="{ '--label-width': labelWidth }"
|
||||
v-bind="$attrs"
|
||||
:server-errors="serverErrors"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<!-- slot 透传 -->
|
||||
@@ -54,7 +56,7 @@
|
||||
|
||||
<el-button
|
||||
v-for="button in moreButtons"
|
||||
v-show="!button.hidden"
|
||||
v-show="!iHidden(button)"
|
||||
:key="button.title"
|
||||
:loading="button.loading"
|
||||
size="small"
|
||||
@@ -70,8 +72,20 @@
|
||||
|
||||
<script>
|
||||
import ElFormRender from './components/el-form-renderer'
|
||||
import { randomString } from '@/utils/string'
|
||||
import { scrollToError } from '@/utils'
|
||||
import { randomString } from '@/utils/common/index'
|
||||
|
||||
const scrollToError = (
|
||||
el,
|
||||
scrollOption = {
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
}
|
||||
) => {
|
||||
setTimeout(() => {
|
||||
const isError = el.getElementsByClassName('is-error')
|
||||
isError[0].scrollIntoView(scrollOption)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -108,6 +122,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
serverErrors: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -128,6 +146,10 @@ export default {
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: '18.2%'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -209,6 +231,9 @@ export default {
|
||||
},
|
||||
getFormValue() {
|
||||
return this.$refs.form.getFormValue()
|
||||
},
|
||||
iHidden(item) {
|
||||
return typeof item.hidden === 'function' ? item.hidden() : item.hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,6 +244,10 @@ export default {
|
||||
margin-right: 80px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-form {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-input-group__prepend {
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -263,7 +292,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
width: 75%;
|
||||
width: calc(100% - var(--label-width));
|
||||
line-height: 32px;
|
||||
|
||||
// 禁用的输入框
|
||||
@@ -337,7 +366,7 @@ export default {
|
||||
|
||||
::v-deep .form-buttons {
|
||||
margin-top: 30px;
|
||||
margin-left: 25%;
|
||||
margin-left: var(--label-width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const Required = {
|
||||
required: true, message: i18n.t('FieldRequiredError'), trigger: 'blur'
|
||||
@@ -118,3 +119,69 @@ export default {
|
||||
matchAlphanumericUnderscore,
|
||||
MatchExcludeParenthesis
|
||||
}
|
||||
/**
|
||||
* @description 表单唯一性校验
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} 列表查询地址
|
||||
* @param {string} 查询参数名
|
||||
* @param {string} 字段中文名
|
||||
* @param {string} 字段名
|
||||
* @param {function(): (string|number)} 返回更新场景下的当前对象 id
|
||||
*/
|
||||
export function UniqueCheck(options = {}) {
|
||||
const { url, param, label, fieldName, getIgnoreId } = options
|
||||
|
||||
function existsInResponse(res) {
|
||||
if (Array.isArray(res)) return res.length > 0
|
||||
if (res && typeof res === 'object') {
|
||||
if (typeof res.count === 'number') return res.count > 0
|
||||
if (Array.isArray(res.results)) return res.results.length > 0
|
||||
}
|
||||
return !!res
|
||||
}
|
||||
|
||||
function extractIds(res) {
|
||||
if (Array.isArray(res)) return res.map(i => i?.id).filter(Boolean)
|
||||
if (res && Array.isArray(res.results)) return res.results.map(i => i?.id).filter(Boolean)
|
||||
return []
|
||||
}
|
||||
|
||||
return {
|
||||
async validator(rule, value, callback) {
|
||||
try {
|
||||
let v = value
|
||||
|
||||
if (typeof v === 'string') v = v.trim()
|
||||
if (v === '' || v === undefined || v === null) return callback()
|
||||
if (!url || !param) return callback()
|
||||
|
||||
const res = await request.get(url, { params: { [param]: v } })
|
||||
let duplicated = existsInResponse(res)
|
||||
|
||||
if (duplicated && typeof getIgnoreId === 'function') {
|
||||
const curId = getIgnoreId()
|
||||
if (curId) {
|
||||
const ids = extractIds(res)
|
||||
|
||||
// 查询结果只包含自身,因此不被视为重复
|
||||
if (ids.length >= 1 && ids.every(id => id === curId)) {
|
||||
duplicated = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicated) {
|
||||
const _label = label || fieldName || ''
|
||||
const msg = `${_label}${i18n.t('Existing')}`
|
||||
callback(new Error(msg))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} catch (e) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<el-button
|
||||
:type="item.el && item.el.type"
|
||||
class="start-stop-btn"
|
||||
:disabled="item.disabled"
|
||||
size="mini"
|
||||
@click="item.callback()"
|
||||
>
|
||||
|
||||
@@ -361,9 +361,10 @@ export default {
|
||||
},
|
||||
// 填充表达式
|
||||
submitFill() {
|
||||
const minMinutes = 60
|
||||
const crontabDiffMin = this.crontabDiff / 1000 / 60
|
||||
if (crontabDiffMin > 0 && crontabDiffMin < 10) {
|
||||
const msg = this.$tc('CrontabDiffError')
|
||||
if (crontabDiffMin > 0 && crontabDiffMin < minMinutes) {
|
||||
const msg = this.$t('CrontabDiffError', { minutes: minMinutes })
|
||||
this.$message.error(msg)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('From') }}
|
||||
<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('Hour') }}
|
||||
<el-input-number v-model="cycle01" :max="23" :min="0" size="mini" />
|
||||
-
|
||||
<el-input-number v-model="cycle02" :max="23" :min="0" size="mini" />
|
||||
{{ this.$t('Hour') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('Every') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" /> {{ this.$t('Hour') }} {{ this.$t('ExecuteOnce') }}
|
||||
<el-input-number v-model="average02" :max="23" :min="1" size="mini" />
|
||||
{{ this.$t('Hour') }} {{ this.$t('ExecuteOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -32,7 +35,7 @@
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item-1 }}</el-option>
|
||||
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('From') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" />
|
||||
<el-input-number v-model="average02" :max="59" :min="1" size="mini" />
|
||||
{{ this.$t('Min') }} {{ this.$t('ExecuteOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script>
|
||||
import parser from 'cron-parser'
|
||||
import { toSafeLocalDateStr } from '@/utils/time'
|
||||
import { toSafeLocalDateStr } from '@/utils/common/time'
|
||||
|
||||
export default {
|
||||
name: 'CrontabResult',
|
||||
|
||||
@@ -39,6 +39,13 @@ export default {
|
||||
showCron: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(val) {
|
||||
this.crontabFill(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
crontabFill(value) {
|
||||
// 确定后回传的值
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script>
|
||||
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
|
||||
import { setUrlParam } from '@/utils/common'
|
||||
import { setUrlParam } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
name: 'ValueFormatter',
|
||||
|
||||
@@ -42,9 +42,9 @@ import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
import ValueFormatter from './ValueFormatter.vue'
|
||||
import AttrFormDialog from './AttrFormDialog.vue'
|
||||
import AttrMatchResultDialog from './AttrMatchResultDialog.vue'
|
||||
import { setUrlParam } from '@/utils/common'
|
||||
import { setUrlParam } from '@/utils/common/index'
|
||||
import { attrMatchOptions } from '@/components/const'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
import { toM2MJsonParams } from '@/utils/jms/index'
|
||||
|
||||
export default {
|
||||
name: 'JSONManyToManySelect',
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
columns: [
|
||||
{ prop: 'name', label: this.$t('AttrName'), formatter: tableFormatter('name') },
|
||||
{ prop: 'match', label: this.$t('Match'), formatter: tableFormatter('match') },
|
||||
{ prop: 'value', label: this.$t('AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs }},
|
||||
{ prop: 'value', label: this.$t('AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs } },
|
||||
{
|
||||
prop: 'action',
|
||||
label: this.$t('Action'),
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [Array, String, Number, Boolean, Object],
|
||||
default: () => ([])
|
||||
default: () => []
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
attrsWithoutValue() {
|
||||
const attrs = Object.assign({}, this.$attrs)
|
||||
const attrs = Object.assign({ clearable: this.clearable }, this.$attrs)
|
||||
delete attrs.value
|
||||
return attrs
|
||||
},
|
||||
@@ -50,6 +50,13 @@ export default {
|
||||
const value = this.objectsToValues(this.value)
|
||||
return value
|
||||
}
|
||||
},
|
||||
clearable() {
|
||||
if (this.$attrs.clearable === undefined) {
|
||||
return this.multiple
|
||||
} else {
|
||||
return this.$attrs.clearable
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -71,8 +78,11 @@ export default {
|
||||
value = value.map(v => {
|
||||
// uuid v4
|
||||
const uuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
|
||||
return typeof v === 'object' ? v
|
||||
: this.$attrs?.allowCreate && !uuid.test(v) ? { [this.customLabelKeyName]: v } : { pk: v }
|
||||
return typeof v === 'object'
|
||||
? v
|
||||
: this.$attrs?.allowCreate && !uuid.test(v)
|
||||
? { [this.customLabelKeyName]: v }
|
||||
: { pk: v }
|
||||
})
|
||||
if (!this.multiple) {
|
||||
value = value[0]
|
||||
@@ -87,9 +97,13 @@ export default {
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val]
|
||||
}
|
||||
val = val.map((v) => {
|
||||
val = val.map(v => {
|
||||
if (v && typeof v === 'object') {
|
||||
return v.pk || v.id || (this.$attrs?.allowCreate ? (v?.[this.customLabelKeyName] + ':' + v?.value) : '')
|
||||
return (
|
||||
v.pk ||
|
||||
v.id ||
|
||||
(this.$attrs?.allowCreate ? v?.[this.customLabelKeyName] + ':' + v?.value : '')
|
||||
)
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
@@ -103,6 +117,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||