Compare commits

..

80 Commits

Author SHA1 Message Date
Bryan
73d6bda8c3 Merge pull request #5092 from jumpserver/dev
merge: v4.10.4-lts
2025-07-17 15:04:47 +08:00
Bryan
3934b45367 Merge pull request #5064 from jumpserver/dev
v4.10.2
2025-06-19 20:13:12 +08:00
Bryan
0c5e84d1e3 Merge pull request #5010 from jumpserver/dev
v4.10.0
2025-05-15 17:12:07 +08:00
Bryan
18a3f42717 Merge pull request #4972 from jumpserver/dev
v4.9.0
2025-04-17 20:18:08 +08:00
Bryan
68030d98b2 Merge pull request #4890 from jumpserver/dev
v4.8.0
2025-03-20 18:44:31 +08:00
Bryan
a861f77609 Merge pull request #4659 from jumpserver/dev
v4.7.0
2025-02-20 10:20:32 +08:00
Bryan
e58ec6057c Merge pull request #4600 from jumpserver/dev
v4.6.0
2025-01-15 14:39:21 +08:00
Bryan
7ab20c5885 Merge pull request #4537 from jumpserver/dev
v4.5.0
2024-12-19 15:57:44 +08:00
Bryan
e47ddb5355 Merge pull request #4451 from jumpserver/dev
v4.4.0
2024-11-21 19:01:11 +08:00
Bryan
56aa3caa83 Merge pull request #4410 from jumpserver/dev
v4.3.0
2024-10-17 14:56:18 +08:00
Bryan
19b1dc0dbc Merge pull request #4367 from jumpserver/dev
merge: from dev to master
2024-09-19 19:36:42 +08:00
Bryan
77ef172a23 Merge pull request #4320 from jumpserver/dev
v4.1.0
2024-08-15 21:42:42 +08:00
Bryan
4596887bf1 Merge pull request #4178 from jumpserver/dev
v4.0.0
2024-07-03 19:06:07 +08:00
Bryan
0a3dc30c85 Merge pull request #4072 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:04:12 +08:00
Bryan
51d24bc8e5 Merge pull request #3941 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:05:04 +08:00
Bryan
1b15a4d043 Merge pull request #3871 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:44:33 +08:00
Bryan
7d3f818242 Merge pull request #3864 from jumpserver/v3.10
v3.10.8
2024-04-18 17:58:05 +08:00
Bryan
4e26f18d77 Merge pull request #3862 from jumpserver/dev
v3.10.8
2024-04-18 17:17:36 +08:00
Bryan
b22613617a Revert "build(deps): bump follow-redirects from 1.15.3 to 1.15.4"
This reverts commit e971cbf4a8.
2024-03-27 16:16:07 +08:00
dependabot[bot]
e971cbf4a8 build(deps): bump follow-redirects from 1.15.3 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 16:11:26 +08:00
wangruidong
4672abae35 fix: 刷新页面根据搜索条件过滤出对应的资源 2024-03-04 19:17:05 +08:00
Bryan
ba36d72602 Merge pull request #3761 from jumpserver/master
v3.10.4 (branch-v3.10)
2024-02-29 16:26:13 +08:00
Bryan
4bfbbba4c5 Merge pull request #3760 from jumpserver/dev
v3.10.4
2024-02-29 16:15:33 +08:00
Bryan
ea038ce43a Merge pull request #3697 from jumpserver/master
v3.10.2
2024-01-17 13:34:12 +00:00
Bryan
e16b19666c Merge pull request #3696 from jumpserver/dev
v3.10.2
2024-01-17 13:33:22 +00:00
Bryan
c7f5409eb6 Merge pull request #3694 from jumpserver/master
v3.10.2
2024-01-17 07:35:33 -04:00
Bryan
fdbd7d2222 Merge pull request #3693 from jumpserver/dev
v3.10.2
2024-01-17 07:24:50 -04:00
Bryan
ddbaeeafea Merge pull request #3668 from jumpserver/master
v3.10.1
2023-12-29 11:34:04 +05:00
Bryan
efb0e9dacb Merge pull request #3665 from jumpserver/dev
v3.10.1
2023-12-29 11:14:54 +05:00
huailei
f6f8301ad5 Revert "perf: 账号收集翻译"
This reverts commit 9a63ae63d4.
2023-12-22 15:25:31 +08:00
“huailei000”
9a63ae63d4 perf: 账号收集翻译 2023-12-22 11:31:45 +08:00
Bryan
1e007ccda3 Merge pull request #3642 from jumpserver/dev
v3.10
2023-12-21 15:15:52 +05:00
老广
d1d0b06b53 Merge pull request #3546 from jumpserver/dev
v3.9.0
2023-11-16 18:25:10 +08:00
Bryan
5fb70d2f24 Merge pull request #3450 from jumpserver/dev
v3.8.0
2023-10-19 03:33:53 -05:00
Bryan
b54a95430f Merge pull request #3404 from jumpserver/dev
v3.7.0
2023-09-21 17:04:42 +08:00
Bryan
4d8b4c45af Merge pull request #3355 from jumpserver/dev
v3.6.0
2023-08-17 14:00:33 +05:00
Bryan
a6d642df60 Merge pull request #3283 from jumpserver/dev
v3.5.0
2023-07-20 19:04:29 +08:00
Jiangjie.Bai
2e74f1522f Merge pull request #3222 from jumpserver/dev
v3.4.0
2023-06-15 14:51:36 +08:00
Jiangjie.Bai
fe615e0314 Merge pull request #3219 from jumpserver/dev
v3.4.0
2023-06-15 14:17:46 +08:00
Jiangjie.Bai
09f734e6fc Merge pull request #3135 from jumpserver/dev
v3.3.0
2023-05-18 19:18:11 +08:00
Jiangjie.Bai
3117046342 Merge pull request #3061 from jumpserver/dev
v3.2.0
2023-04-20 18:40:08 +08:00
Bai
b68aecb5cc fix: 批量更新资产平台help-text 2023-04-20 18:39:22 +08:00
Jiangjie.Bai
1c9b155d97 Merge pull request #3057 from jumpserver/dev
v3.2.0
2023-04-20 18:22:46 +08:00
Jiangjie.Bai
75b1be9864 Merge pull request #3019 from jumpserver/dev
v3.2.0 rc2
2023-04-14 19:01:37 +08:00
Jiangjie.Bai
615c3c1cf4 Merge pull request #3014 from jumpserver/dev
v3.2.0 rc1
2023-04-13 20:02:38 +08:00
Jiangjie.Bai
4d82231af4 Merge pull request #3012 from jumpserver/dev
v3.2.0 rc1
2023-04-13 19:22:38 +08:00
“huailei000”
c6cf6571b6 perf: ldap导入用户列表-组织下拉框设置最大宽度 2023-03-16 16:44:36 +08:00
Bai
8ea990d070 fix: 修复创建资产添加账号模版报错问题 2023-03-16 16:44:36 +08:00
“huailei000”
f4a32170d5 perf: message 2023-03-16 16:44:36 +08:00
ibuler
073508675e perf: 添加默认的信息 2023-03-16 16:44:36 +08:00
Jiangjie.Bai
1d6ca0a93a Merge pull request #2924 from jumpserver/dev
v3.1.0 rc4
2023-03-15 19:46:31 +08:00
Jiangjie.Bai
36aea652d6 Merge pull request #2788 from jumpserver/dev
v3.0.0
2023-02-23 20:16:41 +08:00
Jiangjie.Bai
1a42ce90ab Merge pull request #2760 from jumpserver/dev
v3.0.0-rc-latest
2023-02-22 22:21:54 +08:00
Jiangjie.Bai
31a401b55d Merge pull request #2463 from jumpserver/dev
v3.0.0-rc4
2023-01-31 18:55:34 +08:00
Jiangjie.Bai
582a84178d Merge pull request #2187 from jumpserver/dev
v2.28.0
2022-11-17 17:44:19 +08:00
Jiangjie.Bai
9b9f7c936c Merge pull request #2184 from jumpserver/dev
v2.28.0-rc5
2022-11-17 14:18:15 +08:00
Jiangjie.Bai
2a6100957f Merge pull request #2182 from jumpserver/dev
v2.28.0-rc4
2022-11-16 21:08:55 +08:00
Jiangjie.Bai
16606d6a27 Merge pull request #2176 from jumpserver/dev
v2.28.0-rc2
2022-11-14 10:01:05 +08:00
Jiangjie.Bai
0a612f50e6 Merge pull request #2164 from jumpserver/dev
v2.28.0-rc1
2022-11-10 17:45:47 +08:00
Jiangjie.Bai
fe36fa9390 Merge pull request #2117 from jumpserver/dev
v2.27.0-rc4
2022-10-18 21:02:10 +08:00
Jiangjie.Bai
ba109900ec Merge pull request #2113 from jumpserver/dev
v2.27.0-rc3
2022-10-18 11:20:57 +08:00
Jiangjie.Bai
ec7768267f Merge pull request #2105 from jumpserver/dev
v2.27.0-rc2
2022-10-14 11:01:32 +08:00
Jiangjie.Bai
cc58b374ab Merge pull request #2101 from jumpserver/dev
v2.27.0-rc1
2022-10-13 17:44:53 +08:00
Jiangjie.Bai
04ffbb8fd6 Merge pull request #2097 from jumpserver/dev
v2.27.0-rc1
2022-10-13 15:14:40 +08:00
Jiangjie.Bai
49880f6739 Merge pull request #2059 from jumpserver/dev
v2.26.0
2022-09-15 17:49:44 +08:00
Jiangjie.Bai
e6f98d58c4 Merge pull request #2057 from jumpserver/dev
v2.26.0-rc4
2022-09-15 16:18:03 +08:00
Jiangjie.Bai
fd1f16d43c Merge pull request #2050 from jumpserver/dev
v2.26.0-rc2
2022-09-13 17:41:39 +08:00
Jiangjie.Bai
968b2415b1 Merge pull request #2043 from jumpserver/dev
v2.26.0-rc1
2022-09-08 15:46:44 +08:00
Jiangjie.Bai
776090d6ba Merge pull request #2001 from jumpserver/dev
v2.25.0
2022-08-18 16:12:45 +08:00
Jiangjie.Bai
3a37952288 Merge pull request #1996 from jumpserver/dev
v2.25.0-rc4
2022-08-17 16:53:23 +08:00
Jiangjie.Bai
62b8fc0e3b Merge pull request #1994 from jumpserver/dev
v2.25.0-rc3
2022-08-16 19:08:23 +08:00
Jiangjie.Bai
b2028869cb Merge pull request #1986 from jumpserver/dev
v2.25.0-rc2
2022-08-12 18:06:56 +08:00
Jiangjie.Bai
5277a725f8 Merge pull request #1973 from jumpserver/dev
v2.25.0-rc1
2022-08-11 14:11:59 +08:00
Jiangjie.Bai
f137788c1a Merge pull request #1957 from jumpserver/dev
v2.24.0-rc5
2022-07-20 19:06:03 +08:00
Jiangjie.Bai
f7d17c8de7 Merge pull request #1954 from jumpserver/dev
v2.24.0-rc4
2022-07-19 16:18:13 +08:00
Jiangjie.Bai
feea70b0be Merge pull request #1944 from jumpserver/dev
v2.24.0-rc3
2022-07-18 12:05:42 +08:00
Jiangjie.Bai
04696ef3d6 Merge pull request #1940 from jumpserver/dev
v2.24.0-rc2
2022-07-15 18:07:37 +08:00
Jiangjie.Bai
1731f4f788 Merge pull request #1934 from jumpserver/dev
v2.24.0-rc1
2022-07-14 18:27:51 +08:00
Jiangjie.Bai
6f25d93909 Merge pull request #1931 from jumpserver/dev
v2.24.0-rc1
2022-07-14 17:51:58 +08:00
Jiangjie.Bai
46461ec324 Merge pull request #1925 from jumpserver/dev
v2.24.0-rc1
2022-07-14 15:12:15 +08:00
407 changed files with 7160 additions and 16855 deletions

View File

@@ -14,97 +14,64 @@ 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,
@@ -135,23 +102,17 @@ 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,
@@ -179,126 +140,62 @@ 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'
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
],
'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,
{ anonymous: 'never', named: 'never', asyncArrow: 'always' }
],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [
2,
{
words: true,
nonwords: false
}
],
'object-curly-spacing': [2, 'always'],
'spaced-comment': [
2,
'always',
{
markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}
],
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'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,
'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
}
]
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

View File

@@ -1,57 +0,0 @@
# 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 Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -1,11 +0,0 @@
module.exports = {
singleQuote: true,
semi: false,
trailingComma: 'none',
printWidth: 100,
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'auto'
}

View File

@@ -1,4 +1,4 @@
FROM jumpserver/lina-base:20251204_081759 AS stage-build
FROM jumpserver/lina-base:20250709_063112 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

View File

@@ -1,76 +0,0 @@
# 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` 配置正确。

View File

@@ -1,25 +1,13 @@
import Mock from 'mockjs'
import { param2Obj } from '../src/utils'
import user from './user'
import table from './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]
const mocks = [
...user,
...table
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,

View File

@@ -1,147 +1,148 @@
{
"name": "lina",
"version": "v4.0.0",
"description": "JumpServer Web UI",
"author": "JumpServer Team <support@lxware.hk>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --ext .js,.vue --fix src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@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.2.4",
"echarts": "4.7.0",
"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",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.4",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
"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.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-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",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"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",
"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",
"html-webpack-plugin": "3.2.0",
"husky": "^4.2.3",
"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",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.16.0",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.7.16",
"webpack": "^4.28.4"
},
"engines": {
"node": ">=12",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 4 versions",
"ie 11"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix"
]
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"name": "lina",
"version": "v4.0.0",
"description": "JumpServer Web UI",
"author": "JumpServer Team <support@lxware.hk>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --ext .js,.vue --fix src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@fontsource/open-sans": "^5.0.24",
"@traptitech/markdown-it-katex": "^3.6.0",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"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",
"echarts": "4.7.0",
"elementui-lts": "^2.16.0",
"eslint-plugin-html": "^6.0.0",
"highlight.js": "^11.9.0",
"install": "^0.13.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"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",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "3.3.0",
"sortablejs": "^1.15.6",
"v-sanitize": "^0.0.13",
"vue": "2.6.10",
"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-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",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"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",
"eslint": "^5.15.3",
"eslint-plugin-vue": "5.2.2",
"eslint-plugin-vue-i18n": "^0.3.0",
"github-markdown-css": "^5.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "^4.2.3",
"less-loader": "^5.0.0",
"lint-staged": "^10.1.2",
"mockjs": "1.0.1-beta3",
"pretty-bytes": "^5.6.0",
"runjs": "^4.3.2",
"sass": "~1.32.6",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.16.0",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.6.10"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 4 versions",
"ie 11"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix"
]
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -61,14 +61,13 @@ export function stopJob(form) {
})
}
export function JobUploadFile(form, config = {}) {
export function JobUploadFile(form) {
return request({
url: '/api/v1/ops/jobs/upload/',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60 * 60 * 1000,
data: form,
...config
data: form
})
}

View File

@@ -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'
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
src/assets/img/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/img/deepSeek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -1,6 +1,5 @@
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'
@@ -15,28 +14,19 @@ export const accountFieldsMeta = (vm) => {
}
return {
nodes: {
assets: {
component: Select2,
label: vm.$t('Node'),
label: vm.$t('Asset'),
rules: [Required],
el: {
value: [],
multiple: true,
ajax: {
url: '/api/v1/assets/nodes/',
url: '/api/v1/assets/assets/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
return { label: item.name + '(' + item.address + ')', value: item.id }
}
}
},
hidden: () => {
return !vm.addTemplate
}
},
assets: {
component: AssetSelect,
label: vm.$t('Asset'),
el: {
multiple: false
},
hidden: () => {
return vm.platform || vm.asset
}
@@ -48,7 +38,7 @@ export const accountFieldsMeta = (vm) => {
get disabled() {
return vm.isDisabled
},
multiple: vm.addTemplate,
multiple: false,
ajax: {
url: '/api/v1/accounts/account-templates/',
transformOption: (item) => {

View File

@@ -11,7 +11,7 @@
<script>
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
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('Asset'), ['nodes', 'assets']],
[this.$t('Asset'), ['assets']],
[this.$t('Secret'), [
'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key',

View File

@@ -11,7 +11,7 @@
<script>
import { GenericUpdateFormDialog } from '@/layout/components'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'AccountBulkUpdateDialog',

View File

@@ -93,8 +93,8 @@ export default {
iVisible = true
data = formValue
url = `/api/v1/accounts/accounts/bulk/`
if ((!data.assets || data.assets.length === 0) && (!data.nodes || data.nodes.length === 0)) {
this.$message.error(this.$tc('PleaseSelectAssetOrNode'))
if (data.assets.length === 0) {
this.$message.error(this.$tc('PleaseSelectAsset'))
return
}
}
@@ -107,10 +107,6 @@ 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)
})

View File

@@ -52,7 +52,7 @@
<script>
import { mapGetters } from 'vuex'
import { accountOtherActions, accountQuickFilters, connectivityMeta, isDirectoryServiceAccount } from './const'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
import {
AccountConnectFormatter,
ActionsFormatter,
@@ -341,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
@@ -362,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 && vm.$hasPerm('accounts.verify_account')
!this.$store.getters.currentOrgIsRoot
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
@@ -478,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}/`)
}

View File

@@ -49,10 +49,6 @@ export default {
prop: 'asset',
label: this.$t('Asset')
},
{
prop: 'account',
label: this.$t('Account')
},
{
prop: 'state',
label: this.$t('Status'),

View File

@@ -12,7 +12,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'RemoveAccount',

View File

@@ -23,7 +23,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
export default {

View File

@@ -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/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'ShowSecretInfo',

View File

@@ -1,5 +1,5 @@
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export const connectivityMeta = {
formatter: ChoicesFormatter,
@@ -40,7 +40,7 @@ export function isDirectoryServiceAccount(account, vm) {
return vm.asset && vm.asset.id !== account.asset.id
}
export const accountOtherActions = vm => {
export const accountOtherActions = (vm) => {
return [
{
name: 'View',
@@ -62,11 +62,9 @@ export const accountOtherActions = vm => {
name: 'Update',
title: vm.$t('Edit'),
can: ({ row }) => {
return (
vm.$hasPerm('accounts.change_account') &&
return vm.$hasPerm('accounts.change_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
vm.isUpdateAccount = true
@@ -87,11 +85,9 @@ export const accountOtherActions = vm => {
name: 'UpdateSecret',
title: vm.$t('EditSecret'),
can: ({ row }) => {
return (
vm.$hasPerm('accounts.change_account') &&
return vm.$hasPerm('accounts.change_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
const data = {
@@ -114,11 +110,9 @@ export const accountOtherActions = vm => {
return !vm.asset
},
can: ({ row }) => {
return (
vm.$hasPerm('accounts.add_account') &&
return vm.$hasPerm('accounts.add_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
vm.account = {
@@ -144,26 +138,29 @@ export const accountOtherActions = vm => {
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'])
})
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)
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'))
})
vm.$axios.patch(
`/api/v1/accounts/accounts/clear-secret/`,
{ account_ids: [row.id] }
).then(() => {
vm.$message.success(vm.$tc('ClearSuccessMsg'))
})
}
},
{
@@ -187,11 +184,9 @@ export const accountOtherActions = vm => {
type: 'primary',
divided: true,
can: ({ row }) => {
return (
vm.$hasPerm('accounts.add_account') &&
return vm.$hasPerm('accounts.add_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
has: () => {
return !vm.asset
@@ -209,11 +204,9 @@ export const accountOtherActions = vm => {
title: vm.$t('MoveToAsset'),
type: 'primary',
can: ({ row }) => {
return (
vm.$hasPerm('accounts.delete_account') &&
return vm.$hasPerm('accounts.add_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
has: () => {
return !vm.asset
@@ -229,7 +222,7 @@ export const accountOtherActions = vm => {
]
}
export const accountQuickFilters = vm => [
export const accountQuickFilters = (vm) => [
{
label: vm.$t('Recent (7 days)'),
options: [

View File

@@ -20,7 +20,7 @@
<script>
import TreeTable from '../../Table/TreeTable/index.vue'
import { setRouterQuery, setUrlParam } from '@/utils/common/index'
import { setRouterQuery, setUrlParam } from '@/utils/common'
import $ from '@/utils/jquery-vendor'
export default {

View File

@@ -1,60 +1,12 @@
<template>
<div class="container">
<div class="chat-action">
<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>
&nbsp;&nbsp;&nbsp;
<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 v-if="hasPrompt" class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
v-bind="select"
@change="onSelectChange"
/>
</div>
<div class="chat-input">
<el-input
@@ -85,37 +37,9 @@ export default {
type: Boolean,
default: false
},
modelOptions: {
type: Array,
default: () => []
},
selectedModel: {
type: String,
default: ''
},
loading: {
hasPrompt: {
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
default: true
}
},
data() {
@@ -123,10 +47,15 @@ export default {
isIM: false,
inputValue: '',
select: {
url: '/api/v1/settings/chatai-prompts/',
value: '',
multiple: false,
placeholder: this.$t('Model'),
options: []
placeholder: this.$t('Role'),
ajax: {
transformOption: (item) => {
return { label: item.name, value: item.content }
}
}
}
}
},
@@ -135,32 +64,7 @@ export default {
isLoading: state => state.chat.loading
}),
isSelectDisabled() {
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 || ''
}
return !!this.select.value
}
},
methods: {
@@ -180,25 +84,7 @@ export default {
this.inputValue = ''
},
onSelectChange(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))
this.$emit('select-prompt', value)
}
}
}
@@ -213,18 +99,9 @@ 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: 100%;
width: 50%;
.el-input__inner {
height: 28px;

View File

@@ -3,13 +3,9 @@
<div class="chart-item-container">
<div class="avatar">
<el-avatar
v-if="isUserRole"
:src="userUrl"
:src="isUserRole ? userUrl : chatUrl"
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">
@@ -80,18 +76,16 @@
<script>
import MessageText from './MessageText.vue'
import ModelIcon from '../../models/ModelIcon.vue'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
import { useChat } from '../../useChat.js'
import { reconnect } from '@/utils/request'
import { reconnect } from '@/utils/socket'
const { setLoading, removeLoadingMessageInChat } = useChat()
export default {
components: {
MessageText,
ModelIcon
MessageText
},
props: {
item: {
@@ -99,10 +93,6 @@ export default {
default: () => {
}
},
selectedModel: {
type: String,
default: ''
},
isTerminal: {
type: Boolean,
default: false
@@ -139,8 +129,10 @@ export default {
? this.$i18n.t('ServerBusyRetry')
: ''
},
modelIconName() {
return (this.item?.message?.model || this.selectedModel || this.publicSettings.CHAT_AI_TYPE || '').toString()
chatUrl() {
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
? require('@/assets/img/chat.png')
: require('@/assets/img/deepSeek.png')
}
},
methods: {
@@ -179,18 +171,11 @@ 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 {

View File

@@ -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/index'
import { copy } from '@/utils/common'
export default {
props: {
@@ -40,7 +40,7 @@ export default {
text() {
const value = this.message?.content || ''
if (value && this.markdown) {
return this.renderContentWithDetails(value)
return this.markdown?.render(value)
}
return this.$xss.process(value)
}
@@ -69,7 +69,7 @@ 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) {
@@ -120,77 +120,6 @@ export default {
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) => {
@@ -329,64 +258,4 @@ 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>

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,6 @@
</template>
<script>
import { BASE_URL } from '@/utils/common/index'
export default {
props: {
active: {
@@ -43,16 +41,6 @@ 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
}
}
}

View File

@@ -1,6 +1,5 @@
<template>
<DrawerPanel
v-if="visible"
ref="drawer"
:default-show-panel="!!defaultShowPanel"
:expanded="expanded"
@@ -46,9 +45,9 @@
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 {
@@ -75,66 +74,27 @@ export default {
},
data() {
return {
visible: false,
active: 'chat',
robotUrl: require('@/assets/img/robot-assistant.png'),
height: '400px',
expanded: false,
clientOffset: {},
currentTerminalContent: {},
initialized: false
currentTerminalContent: {}
}
},
computed: {
...mapGetters([
'publicSettings'
])
},
watch: {
'publicSettings.CHAT_AI_METHOD': {
handler(newVal) {
this.visible = newVal === 'api'
}
}
},
mounted() {
this.handleStartChat()
const expanded = aiPannelLocalStorage.get('expanded')
this.updateExpandedState(expanded)
this.handlePostMessage()
},
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()
this.initWebSocket()
return
}
const msg = event.data
@@ -159,6 +119,11 @@ export default {
}
this.$refs.drawer.handleHeaderMoveUp(event)
},
initWebSocket() {
if (!ws) {
this.$refs.component?.init()
}
},
onClose() {
this.$refs.drawer.show = false
},
@@ -172,6 +137,7 @@ export default {
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
@@ -185,8 +151,8 @@ export default {
})
},
onToggle(status) {
this.initWebSocket()
if (status) {
this.initAssistant()
getInputFocus()
}
}

View File

@@ -1,33 +0,0 @@
<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>

View File

@@ -1,45 +0,0 @@
<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>

View File

@@ -1,34 +0,0 @@
<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>

View File

@@ -1,33 +0,0 @@
<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>

View File

@@ -1,33 +0,0 @@
<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>

View File

@@ -1,50 +0,0 @@
<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>

View File

@@ -1,5 +1,5 @@
import store from '@/store'
import { pageScroll } from '@/utils/common/index'
import { pageScroll } from '@/utils/common'
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)
}

View File

@@ -34,7 +34,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'GatewayDialog',

View File

@@ -11,7 +11,6 @@
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',
@@ -35,7 +34,7 @@ export default {
}
const initialUrl = vm.tableConfig.initialUrl
const nodeId = node.meta.data.id
const url = setUrlParam(initialUrl, 'node_id', nodeId)
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
vm.tableConfig.url = url
}
},
@@ -71,7 +70,7 @@ export default {
showMenu: false,
showRefresh: true,
showAssets: false,
showSearch: true,
showSearch: false,
url: this.tableUrl,
// ?assets=0不显示资产. =1显示资产
treeUrl: this.treeUrl,
@@ -79,9 +78,6 @@ export default {
callback: {
onSelected: (event, node) => vm.onSelected(node, vm),
refresh: vm.refreshObjectAssetPermission
},
async: {
enable: false
}
},
tableConfig: {
@@ -89,7 +85,7 @@ export default {
hasTree: true,
columnsExtra: ['view_account'],
columnsExclude: ['spec_info'],
columns: ['id', 'name', 'address', 'comment', 'labels', 'connectivity', 'platform', 'view_account', 'actions'],
columns: ['id', 'name', 'address', 'platform', 'view_account', 'actions'],
columnsShow: {
min: ['name', 'address', 'accounts'],
default: ['name', 'address', 'platform', 'view_account', 'actions']
@@ -113,6 +109,11 @@ export default {
},
connectivity: connectivityMeta,
comment: { ...this.comment }
},
tableAttrs: {
rowClassName({ row }) {
return !row.is_active ? 'row_disabled' : ''
}
}
},
headerActions: {

View File

@@ -37,12 +37,8 @@ export default {
},
headerActions: {
hasExport: true,
hasImport: true,
importOptions: {
encryptFields: [''], // 这里不加密 password''只是为了保证数组有值
canImportUpdate: false
},
hasExport: false,
hasImport: false,
hasCreate: true,
hasSearch: true,
hasRefresh: true,

View File

@@ -6,7 +6,7 @@
<script>
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
@@ -25,7 +25,6 @@ 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: {
@@ -34,7 +33,7 @@ export default {
hasExport: false
},
tableConfig: {
url: `/api/v1/assets/assets/?${key}=${value}&oid=${org_id}`,
url: `/api/v1/assets/assets/?${key}=${value}`,
columns: ['name', 'address', 'platform', 'type', 'is_active'],
columnsShow: {
min: ['name', 'address'],
@@ -46,7 +45,7 @@ export default {
formatter: DetailFormatter,
formatterArgs: {
getRoute: ({ row }) => {
return { name: 'AssetDetail', params: { id: row.id } }
return { name: 'AssetDetail', params: { id: row.id }}
}
}
},

View File

@@ -6,7 +6,7 @@
<script>
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
import { DetailFormatter } from '@/components/Table/TableFormatters'
@@ -25,7 +25,6 @@ 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: {
@@ -34,7 +33,7 @@ export default {
hasExport: false
},
tableConfig: {
url: `/api/v1/users/users/?${key}=${value}&oid=${org_id}`,
url: `/api/v1/users/users/?${key}=${value}`,
columns: [
'name', 'username', 'email', 'groups', 'system_roles',
'org_roles', 'source', 'is_valid'

View File

@@ -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/index'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { openTaskPage } from '@/utils/jms'
import { toSafeLocalDateStr } from '@/utils/time'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {

View File

@@ -8,17 +8,22 @@
:visible.sync="visible"
class="dialog-content"
v-bind="$attrs"
width="600px"
width="740px"
@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') }}
@@ -27,11 +32,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
@@ -44,22 +49,19 @@
</el-select>
</el-col>
</el-row>
<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-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-model="secretValue"
:placeholder="inputPlaceholder"
:show-password="showPassword"
@keyup.enter.native="handleConfirm"
/>
<span
v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'"
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="sendCode"
>
@@ -70,17 +72,21 @@
</el-row>
<el-row>
<el-col>
<iframe v-if="passkeyVisible" :src="passkeyUrl" style="display: none" />
<iframe
v-if="isFaceCaptureVisible && subTypeSelected === 'face' && faceCaptureUrl"
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"
style="width: 100%; height: 600px;border: none;"
/>
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 20px auto 10px">
<el-row :gutter="24" style="margin: 20px auto 10px;">
<el-col :md="24" :sm="24">
<el-button
v-if="!noCodeMFA.includes(subTypeSelected)"
@@ -118,7 +124,7 @@
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'UserConfirmDialog',
@@ -189,65 +195,55 @@ 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}`
},
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
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
@@ -271,26 +267,23 @@ export default {
},
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()
@@ -313,22 +306,16 @@ 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.onSuccess()
})
.catch(err => {
this.$message.error(err.message || this.$tc('ConfirmFailed'))
this.faceCaptureUrl = null
this.isFaceCaptureVisible = false
})
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
})
}
}
}

View File

@@ -1,100 +0,0 @@
<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>

View File

@@ -1,5 +1,5 @@
<script type="text/jsx">
import { toSafeLocalDateStr } from '@/utils/common/time'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'ItemValue',

View File

@@ -12,8 +12,8 @@
<script>
import DetailCard from './index.vue'
import { copy } from '@/utils/common/index'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { copy } from '@/utils/common'
import { toSafeLocalDateStr } from '@/utils/time'
import IBox from '@/components/Common/IBox/index.vue'
import LabelsDetailFormatter from '../Formatters/LabelsDetailFormatter.vue'

View File

@@ -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">
<component
<template
:is="item.component"
v-if="item.component"
v-bind="{...item}"

View File

@@ -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"
@@ -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/index'
import { toSentenceCase } from '@/utils/common'
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,8 +284,6 @@ $color-drop-menu-border: #e4e7ed;
.el-button {
padding: 2px 5px;
line-height: 1.3;
font-size: 13px;
&:not(.is-plain) {
color: $btn-text-color;
@@ -321,6 +319,7 @@ $color-drop-menu-border: #e4e7ed;
// 下拉 options
.el-dropdown-menu {
::v-deep .more-batch-processing {
&:hover {
background-color: transparent !important;
}

View File

@@ -26,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/index'
import { toSentenceCase } from '@/utils/common'
class Action {
constructor() {

View File

@@ -1,6 +1,6 @@
<template>
<div>
<Echart
<div class="content">
<echarts
ref="echarts"
:options="options"
:autoresize="true"
@@ -12,10 +12,9 @@
// 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 },
components: {},
props: {
datesMetrics: {
type: Array,

View File

@@ -1,71 +0,0 @@
<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>

View File

@@ -1,12 +1,14 @@
<template>
<div>
<Echart
<echarts
ref="echarts"
:options="options"
:autoresize="true"
theme="light"
@finished="genSnapshot"
class="disabled-when-print"
@finished="getDataUrl"
/>
<img v-if="dataUrl" :src="dataUrl" class="enabled-when-print" style="display: none;width: 100%;">
</div>
</template>
@@ -14,11 +16,9 @@
// 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,6 +110,7 @@ export default {
},
axisLabel: {
textStyle: {
// 坐标轴颜色
color: '#8F959E'
}
},
@@ -154,7 +155,7 @@ export default {
type: 'line',
smooth: true,
areaStyle: {
// 区域填充样式
// 区域填充样式
normal: {
color: new echarts.graphic.LinearGradient(
0,
@@ -186,7 +187,7 @@ export default {
type: 'line',
smooth: true,
areaStyle: {
// 区域填充样式
// 区域填充样式
normal: {
color: new echarts.graphic.LinearGradient(
0,
@@ -219,58 +220,23 @@ export default {
},
watch: {
range() {
this.genSnapshot()
},
datesMetrics() {
this.genSnapshot()
},
primaryData() {
this.genSnapshot()
},
secondaryData() {
this.genSnapshot()
this.getMetricData()
}
},
mounted() {
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)
}
this.getMetricData()
},
methods: {
forceResize() {
const inst = this.$refs.echarts?.echartsInstance
if (inst) inst.resize()
},
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 = ''
getDataUrl() {
const instance = this.$refs.echarts.echartsInstance
if (instance) {
this.dataUrl = instance.getDataURL()
}
},
getMetricData() {
this.getDataUrl()
}
}
}
</script>
@@ -279,4 +245,16 @@ 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>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<Echart
<echarts
ref="echarts"
:options="options"
:autoresize="true"
@@ -13,10 +13,8 @@
// 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,
@@ -37,15 +35,14 @@ 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
@@ -180,8 +177,8 @@ export default {
</script>
<style lang="scss" scoped>
.echarts {
width: 100%;
height: 72px;
}
.echarts {
width: 100%;
height: 72px;
}
</style>

View File

@@ -9,7 +9,7 @@
class="table"
style="width: 100%"
>
<el-table-column :label="$tc('Ranking')" width="100">
<el-table-column :label="$tc('Ranking')">
<template #header>
<el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500">
<span style="cursor: pointer;">{{ $t('Ranking') }}</span>

View File

@@ -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')
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<Echart
<echarts
ref="echarts"
:autoresize="true"
:options="options"
@@ -13,15 +13,12 @@ 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: {
@@ -36,7 +33,7 @@ export default {
const formatTitle = (text) => {
if (!text) return ''
const maxLength = 25
const maxLength = 23
const lines = []
for (let i = 0; i < text.length; i += maxLength) {
lines.push(text.slice(i, i + maxLength))
@@ -91,7 +88,6 @@ export default {
{
name: title,
type: 'pie',
minAngle: 5,
radius: ['72%', '90%'],
avoidLabelOverlap: false,
itemStyle: {

View File

@@ -16,13 +16,11 @@
<script>
export default {
props: {
name: {
type: String,
default: 'dashboardDays'
},
days: {
type: [String, Number],
default: null
type: String,
default: () => {
return localStorage.getItem('dashboardDays') || '7'
}
},
options: {
type: Array,
@@ -49,28 +47,8 @@ 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)
}
}

View File

@@ -28,7 +28,7 @@
<script>
import i18n from '@/i18n/i18n'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
import Dialog from '@/components/Dialog/index'
export default {

View File

@@ -30,7 +30,7 @@
</template>
<script>
import { getDrawerWidth } from '@/utils/common/index'
import { getDrawerWidth } from '@/utils/common'
export default {
props: {

View File

@@ -5,7 +5,6 @@
ref="dataForm"
:fields="totalFields"
:form="iForm"
:server-errors="serverErrors"
v-bind="$attrs"
v-on="$listeners"
>
@@ -29,7 +28,6 @@
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',
@@ -67,8 +65,7 @@ export default {
totalFields: [],
loading: true,
groups: [],
errors: {},
serverErrors: {}
errors: {}
}
},
computed: {
@@ -97,7 +94,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 })
}
@@ -106,8 +103,6 @@ export default {
this.generateColumns()
this.$emit('afterGenerateColumns', this.totalFields)
this.cleanFormValue()
// 初始化时清空错误
this.serverErrors = {}
this.loading = false
},
generateColumns() {
@@ -115,47 +110,6 @@ 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) {
@@ -185,69 +139,15 @@ 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) {
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' : ''
}
error = error.replace(/[。.]+$/, '')
const field = this.totalFields.find((v) => v.prop === name)
if (!field) {
return
}
// 不写入 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' : ''
})
if (typeof error === 'string') {
field.el.errors = error
field.attrs.error = error
}
},
groupHidden(group, i) {

View File

@@ -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/index'
import { assignIfNot, toSentenceCase } from '@/utils/common'
import TagInput from '@/components/Form/FormFields/TagInput.vue'
import i18n from '@/i18n/i18n'
@@ -177,10 +177,7 @@ 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

View File

@@ -6,7 +6,6 @@
: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">
@@ -20,8 +19,7 @@
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>
@@ -29,7 +27,11 @@
<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>
@@ -48,7 +50,11 @@
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"
@@ -105,10 +111,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) {
@@ -146,10 +152,6 @@ export default {
props: {
// eslint-disable-next-line vue/require-default-prop
data: Object,
serverErrors: {
type: Object,
default: () => ({})
},
prop: {
type: String,
default() {
@@ -168,8 +170,7 @@ export default {
data() {
return {
propsInner: {},
isBlurTrigger:
this.data.rules &&
isBlurTrigger: this.data.rules &&
this.data.rules.some(rule => {
return rule.required && rule.trigger === 'blur'
})
@@ -178,7 +179,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),
// 是否显示
@@ -188,11 +189,6 @@ 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: {
@@ -208,7 +204,10 @@ 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) => {
@@ -233,7 +232,9 @@ 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()
@@ -253,7 +254,8 @@ 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),
@@ -321,7 +323,7 @@ export default {
}
}
</script>
<style lang="scss" scoped>
<style lang='scss' scoped>
.help-tips {
opacity: 0.8;
line-height: 2;
@@ -330,7 +332,7 @@ export default {
.help-block {
::v-deep .el-alert__icon {
font-size: 16px;
font-size: 16px
}
&.checkbox {

View File

@@ -1,18 +1,11 @@
<template>
<el-form
ref="elForm"
:model="value"
class="el-form-renderer"
v-bind="$attrs"
@submit.native.prevent
>
<el-form ref="elForm" :model="value" class="el-form-renderer" v-bind="$attrs">
<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]"
@@ -26,19 +19,13 @@
</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'
@@ -60,10 +47,6 @@ export default {
type: Array,
required: true
},
serverErrors: {
type: Object,
default: () => ({})
},
disabled: {
type: Boolean,
default: false

View File

@@ -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,5 +20,7 @@ export default function getEnableWhenStatus(enableWhen, value) {
})
}
return Array.isArray(enableWhen) ? enableWhen.some(handleCondition) : handleCondition(enableWhen)
return Array.isArray(enableWhen)
? enableWhen.some(handleCondition)
: handleCondition(enableWhen)
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-sequences */
import _ from 'lodash'
import _kebabcase from 'lodash.kebabcase'
/**
* 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,5 +34,8 @@ 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)
]
}

View File

@@ -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,9 +12,11 @@ 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]),
)
}

View File

@@ -8,10 +8,8 @@
:form="basicForm"
:label-position="iLabelPosition"
class="form-fields"
:label-width="labelWidth"
:style="{ '--label-width': labelWidth }"
label-width="25%"
v-bind="$attrs"
:server-errors="serverErrors"
v-on="$listeners"
>
<!-- slot 透传 -->
@@ -56,7 +54,7 @@
<el-button
v-for="button in moreButtons"
v-show="!iHidden(button)"
v-show="!button.hidden"
:key="button.title"
:loading="button.loading"
size="small"
@@ -72,20 +70,8 @@
<script>
import ElFormRender from './components/el-form-renderer'
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)
}
import { randomString } from '@/utils/string'
import { scrollToError } from '@/utils'
export default {
components: {
@@ -122,10 +108,6 @@ export default {
type: Boolean,
default: true
},
serverErrors: {
type: Object,
default: () => ({})
},
fields: {
type: Array,
default: () => []
@@ -146,10 +128,6 @@ export default {
labelPosition: {
type: String,
default: ''
},
labelWidth: {
type: String,
default: '18.2%'
}
},
data() {
@@ -231,9 +209,6 @@ export default {
},
getFormValue() {
return this.$refs.form.getFormValue()
},
iHidden(item) {
return typeof item.hidden === 'function' ? item.hidden() : item.hidden
}
}
}
@@ -244,10 +219,6 @@ export default {
margin-right: 80px;
margin-bottom: 20px;
.el-form {
margin-right: 0;
}
::v-deep .el-input-group__prepend {
border-radius: 0;
}
@@ -292,7 +263,7 @@ export default {
}
.el-form-item__content {
width: calc(100% - var(--label-width));
width: 75%;
line-height: 32px;
// 禁用的输入框
@@ -366,7 +337,7 @@ export default {
::v-deep .form-buttons {
margin-top: 30px;
margin-left: var(--label-width);
margin-left: 25%;
}
}

View File

@@ -1,5 +1,4 @@
import i18n from '@/i18n/i18n'
import request from '@/utils/request'
export const Required = {
required: true, message: i18n.t('FieldRequiredError'), trigger: 'blur'
@@ -119,69 +118,3 @@ 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']
}
}

View File

@@ -14,7 +14,6 @@
<el-button
:type="item.el && item.el.type"
class="start-stop-btn"
:disabled="item.disabled"
size="mini"
@click="item.callback()"
>

View File

@@ -13,7 +13,7 @@
<script>
import parser from 'cron-parser'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'CrontabResult',

View File

@@ -10,7 +10,7 @@
<script>
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
import { setUrlParam } from '@/utils/common/index'
import { setUrlParam } from '@/utils/common'
export default {
name: 'ValueFormatter',

View File

@@ -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/index'
import { setUrlParam } from '@/utils/common'
import { attrMatchOptions } from '@/components/const'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
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'),

View File

@@ -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({ clearable: this.clearable }, this.$attrs)
const attrs = Object.assign({}, this.$attrs)
delete attrs.value
return attrs
},
@@ -50,13 +50,6 @@ 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: {
@@ -78,11 +71,8 @@ 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]
@@ -97,13 +87,9 @@ 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
}
@@ -117,4 +103,6 @@ export default {
}
</script>
<style scoped></style>
<style scoped>
</style>

View File

@@ -46,7 +46,7 @@ export default {
}
},
mounted() {
const defaults = { code: this.getDefaultCode(), phone: '' }
const defaults = { code: localStorage.getItem('prePhoneCode') || '+86', phone: '' }
this.rawValue = this.value || defaults
this.$axios.get('/api/v1/common/countries/').then(res => {
this.countries = res.map(item => {
@@ -56,22 +56,6 @@ export default {
this.$emit('input', this.fullPhone)
},
methods: {
getDefaultCode() {
const mapper = {
'zh': '+86',
'en': '+1',
'ja': '+81',
'ko': '+82',
'fr': '+33',
'de': '+49',
'es': '+34',
'it': '+39',
'ru': '+7',
'ar': '+966'
}
const locale = this.$i18n.locale.split('-')[0]
return localStorage.getItem('prePhoneCode') || mapper[locale] || '+86'
},
onChange(countryCode) {
this.rawValue.code = countryCode
this.onInputChange()

View File

@@ -33,9 +33,6 @@
>
<i :class="[isCheckShowPassword ? 'fa-eye-slash' : 'fa-eye']" class="fa" />
</span>
<span v-if="filterTags.length > 0" class="clear-icon" @click="handleClearAll">
<i class="el-icon-circle-close" :title="$t('Clear')" />
</span>
</div>
</template>
@@ -103,7 +100,7 @@ export default {
methods: {
handleTagClose(tag) {
this.filterTags.splice(this.filterTags.indexOf(tag), 1)
this.$emit('change', this.filterTags)
this.handleConfirm()
},
handleSelect(item) {
this.filterValue = item.value
@@ -143,11 +140,6 @@ export default {
},
handleShowPassword() {
this.isCheckShowPassword = !this.isCheckShowPassword
},
handleClearAll() {
this.filterTags = []
this.$emit('change', this.filterTags)
this.$emit('input', this.filterTags)
}
}
}
@@ -169,12 +161,11 @@ export default {
line-height: 30px;
&:hover {
border-color: #c0c4cc;
border-color: #C0C4CC;
}
& ::v-deep .el-tag {
margin-bottom: 2px;
margin-top: 2px;
margin-bottom: 1px;
font-family: sans-serif !important;
margin-left: 5px;
}
@@ -186,7 +177,6 @@ export default {
.search-input {
flex: 1;
min-width: 150px;
& ::v-deep .el-input__inner {
max-width: 100%;
@@ -214,15 +204,4 @@ export default {
color: #999999;
}
}
.clear-icon {
display: inherit;
padding-right: 6px;
cursor: pointer;
color: #c0c4cc;
&:hover {
color: #606164;
}
}
</style>

View File

@@ -68,7 +68,7 @@ export default {
return { label: item.name, value: item.id }
})
const url = vm.url || vm.ajax.url
const getPageData = async ({ pageIndex, pageSize, keyword }) => {
const getPageData = async({ pageIndex, pageSize, keyword }) => {
const limit = pageSize
const offset = (pageIndex - 1) * pageSize
const params = {

View File

@@ -24,7 +24,7 @@
<script>
import Select2 from './Select2.vue'
import { hasUUID } from '@/utils/common/index'
import { hasUUID } from '@/utils/common'
export default {
components: {

View File

@@ -20,7 +20,7 @@
</template>
<script>
import { randomString } from '@/utils/common/index'
import { randomString } from '@/utils/string'
export default {
props: {

View File

@@ -23,7 +23,7 @@
<script type="text/jsx">
import Sortable from 'sortablejs'
import DataTable from '@/components/Table/DataTable/index.vue'
import { newURL, ObjectLocalStorage, replaceAllUUID } from '@/utils/common/index'
import { newURL, ObjectLocalStorage, replaceAllUUID } from '@/utils/common'
import ColumnSettingPopover from './components/ColumnSettingPopover.vue'
import { TableColumnsGenerator } from './utils'
@@ -60,24 +60,25 @@ export default {
},
isDeactivated: false,
tableColumnsStorage: this.getTableColumnsStorage(),
sortable: null,
inited: false
sortable: null
}
},
watch: {
config: {
immediate: false,
handler: _.debounce(function(iNew, iOld) {
if (this.isDeactivated || !this.inited) {
if (this.isDeactivated) {
return
}
const changed = this.isConfigChanged(iNew, iOld)
if (!changed) {
return
try {
if (JSON.stringify(iNew) === JSON.stringify(iOld)) {
return
}
} catch (error) {
this.$log.error('JsonStringify Error: ', error)
}
this.optionUrlMetaAndGenCols()
this.$log.debug('AutoDataTable Config change found')
this.$log.debug('AutoDataTable Config change found, ', this.isDeactivated)
}, 200)
}
},
@@ -92,35 +93,6 @@ export default {
this.isDeactivated = false
},
methods: {
isConfigChanged(iNew, iOld) {
const _iNew = _.cloneDeep(iNew)
const _iOld = _.cloneDeep(iOld)
delete _iNew.columns
delete _iOld.columns
const oldMeta = _iNew.columnsMeta
const newMeta = _iOld.columnsMeta
const metas = [oldMeta, newMeta]
for (const meta of metas) {
if (!meta) {
continue
}
for (const [key, value] of Object.entries(meta)) {
if (!key || !value || typeof value !== 'object') {
continue
}
delete value['formatter']
}
}
try {
if (JSON.stringify(_iNew) === JSON.stringify(_iOld)) {
return false
}
} catch (error) {
this.$log.error('JsonStringify Error: ', error)
}
return true
},
setColumnDraggable() {
const el = this.$el.querySelector('.el-table__header-wrapper thead tr')
if (!el) {
@@ -266,7 +238,6 @@ export default {
if (this.$refs.dataTable) {
this.$refs.dataTable.getList()
}
this.inited = true
})
},
orderingColumns(columns) {

View File

@@ -1,4 +1,4 @@
import { toSentenceCase } from '@/utils/common/index'
import { toSentenceCase } from '@/utils/common'
import i18n from '@/i18n/i18n'
import {
@@ -225,15 +225,11 @@ export class TableColumnsGenerator {
padding = '6px'
value = '-'
}
return h(
'span',
{
style: {
marginLeft: padding
}
},
[value]
)
return h('span', {
'style': {
marginLeft: padding
}
}, [value])
}
}
return col
@@ -285,11 +281,10 @@ export class TableColumnsGenerator {
}
return (
<span>
{column.label}
<span>{column.label}
<el-tooltip {...binds}>
<div slot='content' v-sanitize={helpTip} />
<i class='fa fa-question-circle-o help-tip-icon' style='padding-left: 2px' />
<div slot='content' v-sanitize={helpTip}/>
<i class='fa fa-question-circle-o help-tip-icon' style='padding-left: 2px'/>
</el-tooltip>
</span>
)
@@ -304,13 +299,16 @@ export class TableColumnsGenerator {
return col
}
if (column.type === 'boolean') {
col.filters = [{ text: i18n.t('Yes'), value: true }, { text: i18n.t('No'), value: false }]
col.filters = [
{ text: i18n.t('Yes'), value: true },
{ text: i18n.t('No'), value: false }
]
col.sortable = false
col['column-key'] = col.prop
}
if (column.type === 'choice' && column.choices) {
col.filters = column.choices.map(item => {
if (typeof item.value === 'boolean') {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item['label'], value: 'True' }
} else {

View File

@@ -99,9 +99,9 @@ export default {
totalData: [],
page: defaultFirstPage,
extraQuery: {},
paginationSize: 12,
paginationSize: 6,
paginationLayout: 'total, sizes, prev, pager, next',
paginationSizes: [12, 24, 36],
paginationSizes: [6, 18, 27],
loading: true,
axiosConfig: {
raw: 1,
@@ -212,7 +212,7 @@ export default {
this.$confirm(msg, this.$tc('Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async (action, instance, done) => {
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
const deleteFunc = this.tableConfig.onDelete || this.defaultPerformDelete
await deleteFunc(obj)
@@ -261,7 +261,7 @@ export default {
min-width: 330px;
position: relative;
margin-bottom: 20px;
height: 180px;
//height: 230px;
width: 380px;
padding: 15px;

View File

@@ -5,11 +5,6 @@
<slot name="no-data" />
</template>
<template v-else>
<!--
过滤 selection 相关事件的透传避免父组件收到 el-table 原生的当前页 selection
导致跨页选择persistSelection被覆盖只剩当页数据
选择事件统一走 selectStrategy在内部维护全量 selected 并向外 emit
-->
<el-table
ref="table"
v-loading="tableLoading"
@@ -17,7 +12,7 @@
:row-class-name="rowClassName"
v-bind="tableAttrs"
@select="selectStrategy.onSelect"
v-on="forwardListeners"
v-on="$listeners"
@selection-change="selectStrategy.onSelectionChange"
@select-all="handleSelectAll($event, canSelect)"
@sort-change="onSortChange"
@@ -28,18 +23,27 @@
<template v-if="hasSelect">
<el-data-table-column
key="selection-key"
v-bind="{ align: columnsAlign, ...columns[0] }"
v-bind="{align: columnsAlign, ...columns[0]}"
/>
<el-data-table-column key="tree-ctrl" v-bind="{ align: columnsAlign, ...columns[1] }">
<el-data-table-column
key="tree-ctrl"
v-bind="{align: columnsAlign, ...columns[1]}"
>
<template slot-scope="scope">
<span v-for="space in scope.row._level" :key="space" class="ms-tree-space" />
<span
v-for="space in scope.row._level"
:key="space"
class="ms-tree-space"
/>
<span
v-if="iconShow(scope.$index, scope.row)"
class="tree-ctrl"
@click="toggleExpanded(scope.$index)"
>
<i :class="`el-icon-${scope.row._expanded ? 'minus' : 'plus'}`" />
<i
:class="`el-icon-${scope.row._expanded ? 'minus' : 'plus'}`"
/>
</span>
{{ scope.row[columns[1].prop] }}
</template>
@@ -48,16 +52,23 @@
<el-data-table-column
v-for="col in columns.filter((c, i) => i !== 0 && i !== 1)"
:key="col.prop"
v-bind="{ align: columnsAlign, ...col }"
v-bind="{align: columnsAlign, ...col}"
/>
</template>
<!--无选择-->
<template v-else>
<!--展开这列, 丢失 el-data-table-column属性-->
<el-data-table-column key="tree-ctrl" v-bind="{ align: columnsAlign, ...columns[0] }">
<el-data-table-column
key="tree-ctrl"
v-bind="{align: columnsAlign, ...columns[0]}"
>
<template slot-scope="scope">
<span v-for="space in scope.row._level" :key="space" class="ms-tree-space" />
<span
v-for="space in scope.row._level"
:key="space"
class="ms-tree-space"
/>
<span
v-if="iconShow(scope.$index, scope.row)"
@@ -73,19 +84,14 @@
<el-data-table-column
v-for="col in columns.filter((c, i) => i !== 0)"
:key="col.prop"
v-bind="{ align: columnsAlign, ...col }"
v-bind="{align: columnsAlign, ...col}"
/>
</template>
</template>
<!--非树-->
<template v-else>
<el-data-table-column
v-if="hasSelection"
:align="selectionAlign"
:selectable="canSelect"
type="selection"
/>
<el-data-table-column v-if="hasSelection" :align="selectionAlign" :selectable="canSelect" type="selection" />
<el-data-table-column
v-for="col in columns"
:key="col.prop"
@@ -94,14 +100,14 @@
:filters="col.filters || null"
:formatter="typeof col.formatter === 'function' ? col.formatter : null"
:title="col.label"
v-bind="{ align: columnsAlign, ...col }"
v-bind="{align: columnsAlign, ...col}"
>
<template #header>
<span :title="col.label">{{ col.label }}</span>
</template>
<template
v-if="col.formatter && typeof col.formatter !== 'function'"
v-slot:default="{ row, column, $index }"
v-slot:default="{row, column, $index}"
>
<div
:is="col.formatter"
@@ -155,9 +161,9 @@
</template>
<script>
import _get from 'lodash/get'
import _values from 'lodash/values'
import _isEmpty from 'lodash/isEmpty'
import _get from 'lodash.get'
import _values from 'lodash.values'
import _isEmpty from 'lodash.isempty'
import SelfLoadingButton from './components/self-loading-button.vue'
import TheDialog, { dialogModes } from './components/the-dialog.vue'
import ElDataTableColumn from './components/el-data-table-column'
@@ -269,7 +275,8 @@ export default {
*/
beforeSearch: {
type: Function,
default() {}
default() {
}
},
/**
* 单选, 适用场景: 不可以批量删除
@@ -434,7 +441,8 @@ export default {
*/
onEdit: {
type: Function,
default(row) {}
default(row) {
}
},
/**
* 点击删除按钮时的方法, 当默认删除方法不满足需求时使用, 需要返回promise
@@ -443,7 +451,9 @@ export default {
onDelete: {
type: Function,
default(data) {
const ids = Array.isArray(data) ? data.map(v => v[this.id]).join(',') : data[this.id]
const ids = Array.isArray(data)
? data.map(v => v[this.id]).join(',')
: data[this.id]
return this.$axios.delete(this.url + '/' + ids + '/', this.axiosConfig)
}
},
@@ -697,8 +707,8 @@ export default {
}
},
/*
* 设置默认对齐方式
*/
* 设置默认对齐方式
*/
defaultAlign: {
type: String,
default: 'center'
@@ -713,7 +723,8 @@ export default {
},
extraPaginationAttrs: {
type: Object,
default: () => {}
default: () => {
}
},
hasSelection: {
type: Boolean,
@@ -799,16 +810,6 @@ export default {
selectStrategy() {
return getSelectStrategy(this)
},
// 过滤会与内部选择策略冲突的事件,避免父组件只拿到当前页 selection
forwardListeners() {
const listeners = { ...this.$listeners }
delete listeners['selection-change']
delete listeners['select']
delete listeners['select-all']
// 外层如需监听 selection 变化,请监听本组件透出的 selection-change
// 该事件来自选择策略,已汇总跨页后的全量 selected
return listeners
},
searchLocatedSlotKeys() {
return getLocatedSlotKeys(this.$slots, 'search:')
},
@@ -896,7 +897,9 @@ export default {
}
Object.assign(query, this._extraQuery)
Object.assign(query, this.innerQuery)
query[this.pageSizeKey] = this.hasPagination ? this.size : this.noPaginationSize
query[this.pageSizeKey] = this.hasPagination
? this.size
: this.noPaginationSize
// 根据偏移值计算接口正确的页数
const pageOffset = this.firstPage - defaultFirstPage
@@ -978,7 +981,9 @@ export default {
formValue = this.$refs.searchForm.getFormValue()
Object.assign(query, formValue)
}
const queryStr = (url.indexOf('?') > -1 ? '&' : '?') + queryUtil.stringify(query, '=', '&')
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
// 请求开始
this.tableLoading = loading
@@ -998,7 +1003,10 @@ export default {
// 不分页
if (!this.hasPagination) {
data = _get(resp, this.dataPath) || _get(resp, noPaginationDataPath) || []
data =
_get(resp, this.dataPath) ||
_get(resp, noPaginationDataPath) ||
[]
this.total = data.length
} else {
data = _get(resp, this.dataPath) || []
@@ -1181,7 +1189,7 @@ export default {
this.$confirm(this.deleteMessage(data), this.$t('Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async (action, instance, done) => {
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
instance.confirmButtonLoading = true
@@ -1217,7 +1225,11 @@ export default {
}
const remain = this.data.length - deleteCount
const lastPage = Math.ceil(this.total / this.size)
if (remain === 0 && this.page === lastPage && this.page > defaultFirstPage) {
if (
remain === 0 &&
this.page === lastPage &&
this.page > defaultFirstPage
) {
this.page--
}
},
@@ -1245,14 +1257,20 @@ export default {
tmp.push(record)
if (record[this.treeChildKey] && record[this.treeChildKey].length > 0) {
const children = this.tree2Array(record[this.treeChildKey], expandAll, record, _level)
const children = this.tree2Array(
record[this.treeChildKey],
expandAll,
record,
_level
)
tmp = tmp.concat(children)
}
})
return tmp
},
rowClassName(...args) {
let rcn = this.tableAttrs.rowClassName || this.tableAttrs['row-class-name'] || ''
let rcn =
this.tableAttrs.rowClassName || this.tableAttrs['row-class-name'] || ''
if (typeof rcn === 'function') rcn = rcn(...args)
if (this.isTree) rcn += ' ' + this.showRow(...args)
return rcn

View File

@@ -1,9 +1,14 @@
/**
* 两种多选策略Normal 和 PersistSelection
*/
/**
* 多选策略接口
*/
class StrategyAbstract {
constructor(elDataTable) {
this.elDataTable = elDataTable
// 绑定this后可直接在template中使用
this.onSelectionChange = this.onSelectionChange.bind(this)
this.onSelect = this.onSelect.bind(this)
this.onSelectAll = this.onSelectAll.bind(this)
@@ -13,22 +18,39 @@ class StrategyAbstract {
return this.elDataTable.$refs.table
}
onSelectionChange() {}
onSelect() {}
onSelectAll() {}
toggleRowSelection() {}
clearSelection() {}
updateElTableSelection() {}
onSelectionChange() {
}
onSelect() {
}
onSelectAll() {
}
toggleRowSelection() {
}
clearSelection() {
}
updateElTableSelection() {
}
}
/**
* 普通策略。由 el-table 自己维护 selection
* 普通策略。由el-table维护selected
*/
class StrategyNormal extends StrategyAbstract {
/**
* normal模式下只需要监听selection-change事件
*/
onSelectionChange(val) {
this.elDataTable.selected = val
}
/**
* toggleRowSelection和clearSelection的表现与el-table一致
*/
toggleRowSelection(...args) {
return this.elTable.toggleRowSelection(...args)
}
@@ -39,28 +61,44 @@ class StrategyNormal extends StrategyAbstract {
}
/**
* 跨页保存多选策略
* 跨页保存多选策略。手动维护selected数组
*/
class StrategyPersistSelection extends StrategyAbstract {
/**
* el-table的selection-change事件不适用于开启跨页保存的情况。
* 比如当开启persistSelection时发生以下两个场景
* 1. 用户点击翻页
* 2. 用户点击行首的切换全选项按钮,清空当前页多选项数据
* 其中场景1应该保持selected不变而场景2只应该从selected移除当前页所有行保留其他页面的多选状态。
* 但el-table的selection-change事件在两个场景中无差别发生所以这里不处理这个事件
*/
/**
* 用户切换某一行的多选
*/
onSelect(selection, row) {
const isChosen = selection.indexOf(row) > -1
this.toggleRowSelection(row, isChosen)
// el-table 原生 selection-change 仅包含当前页。为保证跨页勾选有效,
// 在内部策略维护完 selected 后,向外部同步“全量已选”。
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
}
/**
* 用户切换当前页的多选
*/
onSelectAll(selection, selectable = () => true) {
const { id, selected, data } = this.elDataTable
const selectableRows = data.filter(selectable)
// const isSelected = !!selection.length
// 创建已选择项的 id 集合,用于快速查找
const selectedIds = new Set(selected.map(r => r[id]))
const currentPageIds = new Set(selectableRows.map(row => row[id]))
// 前页面的选中状态
const currentPageSelectedCount = selectableRows.filter(row =>
selectedIds.has(row[id])
).length
// 判断是全选还是取消全选
const shouldSelectAll = currentPageSelectedCount < selectableRows.length
this.elTable?.clearSelection()
@@ -68,11 +106,15 @@ class StrategyPersistSelection extends StrategyAbstract {
if (shouldSelectAll) {
selectableRows.forEach(row => {
if (!selectedIds.has(row[id])) selected.push(row)
this.elTable.toggleRowSelection(row, true)
// ! 这里需要触发事件,否则在 el-table 中无法触发 selection-change 事件
this.elDataTable.$emit('toggle-row-selection', true, row)
})
} else {
const newSelected = []
selected.forEach(row => {
if (!currentPageIds.has(row[id])) {
newSelected.push(row)
@@ -80,12 +122,17 @@ class StrategyPersistSelection extends StrategyAbstract {
this.elDataTable.$emit('toggle-row-selection', false, row)
}
})
this.elDataTable.selected = newSelected
}
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
}
/**
* toggleRowSelection和clearSelection管理elDataTable的selected数组
* 记得最后要将状态同步到el-table中
*/
toggleRowSelection(row, isSelected) {
const { id, selected } = this.elDataTable
const foundIndex = selected.findIndex(r => r[id] === row[id])
@@ -102,24 +149,26 @@ class StrategyPersistSelection extends StrategyAbstract {
this.elDataTable.$emit('toggle-row-selection', isSelected, row)
this.updateElTableSelection()
// 切换后同步全量 selection跨页
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
}
clearSelection() {
this.elDataTable.selected = []
this.updateElTableSelection()
// 清空后也同步给外部,保持外层状态一致
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
}
/**
* 将selected状态同步到el-table中
*/
updateElTableSelection() {
const { data, id, selected } = this.elDataTable
const selectedIds = new Set(selected.map(r => r[id]))
this.elTable?.clearSelection()
data.forEach(row => {
const shouldBeSelected = selectedIds.has(row[id])
if (!this.elTable) return
if (shouldBeSelected) {
this.elTable.toggleRowSelection(row, true)
}

View File

@@ -10,7 +10,7 @@
</template>
<script>
import { newURL, ObjectLocalStorage } from '@/utils/common/index'
import { newURL, ObjectLocalStorage } from '@/utils/common'
import { default as ElDatableTable } from './compenents/el-data-table'
import { mapGetters } from 'vuex'
@@ -65,7 +65,7 @@ export default {
onEdit: (row) => {
const defaultOnEdit = (row) => {
const routeName = userTableActions.editRoute
this.$router.push({ name: routeName, params: { id: row.id } })
this.$router.push({ name: routeName, params: { id: row.id }})
}
let onEdit = userTableActions.onEdit
if (!onEdit) {

View File

@@ -22,9 +22,8 @@
<script>
import ListTable from '../ListTable'
import Drawer from '@/components/Drawer/index.vue'
import { setUrlParam, toLowerCaseExcludeAbbr, toSentenceCase } from '@/utils/common/index'
import { setUrlParam, toLowerCaseExcludeAbbr, toSentenceCase } from '@/utils/common'
import { mapGetters } from 'vuex'
import { resolveRoute } from '@/utils/vue/index'
const drawerType = [String, Function]
@@ -107,8 +106,8 @@ export default {
const formatterArgs = value?.formatterArgs
// console.log('>>> name: ', key)
// console.log('>>> formatter: ', formatter)
const detailFormatters = ['AmountFormatter', 'DetailFormatter']
if (formatter && detailFormatters.includes(formatter.name) && formatterArgs.drawer !== false) {
const detailFormaters = ['AmountFormatter', 'DetailFormatter']
if (formatter && detailFormaters.includes(formatter.name) && formatterArgs.drawer !== false) {
formatterArgs.onClick = this.onDetail
}
}
@@ -167,9 +166,9 @@ export default {
}
this.drawerComponent = ''
},
getDetailDrawerTitle({ col, row, cellValue, payload = {} }) {
getDetailDrawerTitle({ col, row, cellValue, payload = {}}) {
this.$log.debug('>>> getDetailDrawerTitle: ', col, row, cellValue, payload)
const { detailRoute = {}, formatterArgs = {} } = payload
const { detailRoute = {}, formatterArgs = {}} = payload
const getTitle = formatterArgs.getDrawerTitle
this.$log.debug('>>> getTitle: ', getTitle)
if (getTitle && typeof getTitle === 'function') {
@@ -178,7 +177,7 @@ export default {
if (formatterArgs.title) {
return formatterArgs.title
}
const resolvedRoute = resolveRoute(detailRoute, this.$router)
const resolvedRoute = this.resolveRoute(detailRoute)
let title = cellValue || row.name
if (formatterArgs.getTitle) {
title = formatterArgs.getTitle({ col, row, cellValue })
@@ -223,6 +222,7 @@ export default {
}
title = actionLabel + this.$t('WordSep') + toLowerCaseExcludeAbbr(title)
return title
},
getDefaultDrawer(action) {
@@ -232,38 +232,53 @@ export default {
},
getRouteNameComponent(name, action) {
const route = { name: name }
if (action === 'detail' || action === 'update') {
route.params = { id: '1' }
}
const resolved = resolveRoute(route, this.$router)
if (resolved && resolved.components && resolved.components.default) {
return resolved.components.default
const routes = this.$router.resolve(route)
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === name && item.components)
if (matched.length === 0) {
return
}
return ''
if (matched[0] && matched[0].components?.default) {
const component = matched[0].components.default
return component
}
},
resolveRoute(route) {
const routes = this.$router.resolve(route)
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === route.name && item.components)
if (matched.length === 0) {
return
}
if (matched[0] && matched[0].components?.default) {
return matched[0]
}
},
getDetailComponent({ detailRoute }) {
if (!detailRoute) {
return this.detailDrawer
}
const route = resolveRoute(detailRoute, this.$router)
this.$log.debug('>>> getDetailComponent: ', detailRoute)
const route = this.resolveRoute(detailRoute)
let component = null
if (route && route.components && route.components.default) {
if (route) {
component = route.components.default
}
if (!component) {
component = this.detailDrawer
}
return component
},
getDrawerComponent(action, payload) {
this.$log.debug('>>> getDrawerComponent: ', action, payload)
switch (action) {
case 'create':
return this.createDrawer
@@ -278,7 +293,7 @@ export default {
}
},
async showDrawer(action, { row = {}, col = {}, query = {}, cellValue = '', payload = {} } = {}) {
async showDrawer(action, { row = {}, col = {}, query = {}, cellValue = '', payload = {}} = {}) {
try {
// 1. 先重置状态
this.drawerVisible = false
@@ -294,6 +309,7 @@ export default {
// 3. 设置组件
this.drawerComponent = this.getDrawerComponent(action, payload)
this.$log.debug('>>> drawerComponent: ', this.drawerComponent)
this.drawerTitle = this.getActionDrawerTitle({ action, row, col, cellValue, payload })
// 4. 如果没有组件,尝试获取默认组件
if (!this.drawerComponent) {
@@ -310,7 +326,6 @@ export default {
const actionMeta = await this.$store.getters['common/drawerActionMeta']
this.title = this.getDrawerTitle({ action, ...actionMeta })
}
this.drawerTitle = this.getActionDrawerTitle({ action, row, col, cellValue, payload })
// 7. 等待下一个 tick确保组件已设置
await this.$nextTick()
@@ -355,7 +370,7 @@ export default {
await this.$store.dispatch('common/setDrawerActionMeta', {
action: 'detail', row: row, col: col, id: id
})
await this.showDrawer('detail', { row, col, cellValue, payload: { detailRoute, formatterArgs } })
await this.showDrawer('detail', { row, col, cellValue, payload: { detailRoute, formatterArgs }})
},
async onCreate(meta) {
if (!meta) {
@@ -365,14 +380,14 @@ export default {
await this.$store.dispatch('common/setDrawerActionMeta', { action: 'create', ...meta })
await this.showDrawer('create', meta)
},
async onClone({ row, col, query = {} }) {
async onClone({ row, col, query = {}}) {
this.$route.params.id = ''
await this.$store.dispatch('common/setDrawerActionMeta', {
action: 'clone', row: row, col: col, id: row.id
})
await this.showDrawer('clone', { query })
},
async onUpdate({ row, col, query = {} }) {
async onUpdate({ row, col, query = {}}) {
this.$route.params.id = row.id
this.$route.params.action = 'update'
await this.$store.dispatch('common/setDrawerActionMeta', {

View File

@@ -49,7 +49,7 @@
import Dialog from '@/components/Dialog/index.vue'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/Table/DataTable/compenents/el-data-table/utils/query'
import { download } from '@/utils/common/index'
import { download } from '@/utils/common'
export default {
name: 'ExportDialog',

View File

@@ -58,7 +58,6 @@
:import-option="importOption"
:json-data="jsonData"
:url="url"
v-bind="$attrs"
@cancel="cancelUpload"
@finish="closeDialog"
/>
@@ -69,7 +68,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import ImportTable from '@/components/Table/ListTable/TableAction/ImportTable.vue'
import { download, getErrorResponseMsg } from '@/utils/common/index'
import { download, getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
export default {
@@ -248,46 +247,46 @@ export default {
</script>
<style lang='scss' scoped>
@import "~@/styles/variables";
@import "~@/styles/variables";
.error-msg {
color: $--color-danger;
}
.error-msg.error-results {
background-color: #f3f3f4;
max-height: 200px;
overflow: auto
}
.file-uploader ::v-deep .el-upload {
width: 100%;
//padding-right: 150px;
}
.file-uploader ::v-deep .el-upload-dragger {
width: 100%;
}
.importTableZone {
padding: 0 20px;
.importTable {
overflow: auto;
.error-msg {
color: $--color-danger;
}
.tableFilter {
padding-bottom: 10px;
.error-msg.error-results {
background-color: #f3f3f4;
max-height: 200px;
overflow: auto
}
}
.importTable ::v-deep .el-dialog__body {
padding-bottom: 20px;
}
.file-uploader ::v-deep .el-upload {
width: 100%;
//padding-right: 150px;
}
.export-item {
margin-left: 80px;
}
.file-uploader ::v-deep .el-upload-dragger {
width: 100%;
}
.importTableZone {
padding: 0 20px;
.importTable {
overflow: auto;
}
.tableFilter {
padding-bottom: 10px;
}
}
.importTable ::v-deep .el-dialog__body {
padding-bottom: 20px;
}
.export-item {
margin-left: 80px;
}
.export-item:first-child {
margin-left: 0;

View File

@@ -42,10 +42,10 @@
<script>
import DataTable from '@/components/Table/DataTable/index.vue'
import { getUpdateObjURL } from '@/utils/common/index'
import { sleep } from '@/utils/common/time'
import { getUpdateObjURL } from '@/utils/common'
import { sleep } from '@/utils/time'
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
export default {
@@ -97,10 +97,6 @@ export default {
origin: {
type: String,
default: ''
},
encryptFields: {
type: Array,
default: () => []
}
},
data() {
@@ -277,15 +273,11 @@ export default {
}
return columns
},
getEncryptFields() {
const fromProp = Array.isArray(this.encryptFields) && this.encryptFields.length ? this.encryptFields : null
return fromProp || ['password', 'secret', 'private_key']
},
generateTableData(tableTitles, tableData) {
const totalData = []
tableData.forEach(item => {
this.$set(item, '@status', 'pending')
const encryptFields = this.getEncryptFields()
const encryptFields = ['password', 'secret', 'private_key']
for (const field of encryptFields) {
if (item[field]) {
item[field] = encryptPassword(item[field])

View File

@@ -10,7 +10,7 @@
<script>
import { cleanActions } from './utils'
import { createSourceIdCache } from '@/api/common'
import { getErrorResponseMsg } from '@/utils/common/index'
import { getErrorResponseMsg } from '@/utils/common'
import i18n from '@/i18n/i18n'
import DataActions from '@/components/Common/DataActions/index.vue'
@@ -240,7 +240,7 @@ export default {
type: 'warning',
confirmButtonClass: 'el-button--danger',
showCancelButton: true,
beforeClose: async (action, instance, done) => {
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
instance.confirmButtonLoading = true
try {

View File

@@ -45,7 +45,7 @@
<script>
import SummaryCard from '@/components/Cards/SummaryCard/index.vue'
import { setUrlParam } from '@/utils/common/index'
import { setUrlParam } from '@/utils/common'
export default {
name: 'QuickFilter',

View File

@@ -17,7 +17,7 @@
import ActionsGroup from '@/components/Common/ActionsGroup/index.vue'
import ImExportDialog from './ImExportDialog.vue'
import { cleanActions } from './utils'
import { assignIfNot } from '@/utils/common/index'
import { assignIfNot } from '@/utils/common'
const defaultTrue = { type: [Boolean, Function, String], default: true }

View File

@@ -49,7 +49,7 @@ import LeftSide from './LeftSide.vue'
import RightSide from './RightSide.vue'
import AutoDataSearch from '@/components/Table/AutoDataSearch/index.vue'
import DatetimeRangePicker from '@/components/Form/FormFields/DatetimeRangePicker.vue'
import { getDaysAgo, getDaysFuture } from '@/utils/common/time'
import { getDaysAgo, getDaysFuture } from '@/utils/time'
import LabelSearch from '@/components/Table/ListTable/TableAction/LabelSearch.vue'
const defaultTrue = { type: Boolean, default: true }
@@ -86,11 +86,13 @@ export default {
},
datePick: {
type: Function,
default: val => {}
default: (val) => {
}
},
searchTable: {
type: Function,
default: val => {}
default: (val) => {
}
},
selectedRows: {
type: Array,
@@ -151,7 +153,7 @@ export default {
}
</script>
<style lang="scss" scoped>
<style lang='scss' scoped>
$innerHeight: 28px;
$headerHeight: 30px;
@@ -237,4 +239,5 @@ $headerHeight: 30px;
display: block;
padding: 5px 20px;
}
</style>

View File

@@ -9,7 +9,7 @@
/>
<TableAction
v-if="hasActions"
:class="{ 'filter-expand': filterExpand }"
:class="{'filter-expand': filterExpand}"
:date-pick="handleDateChange"
:has-quick-filter="iHasQuickFilter"
:quick-filter-expand.sync="filterExpand"
@@ -34,7 +34,7 @@
</template>
<script>
import { getResourceFromApiUrl } from '@/utils/jms/index'
import { getResourceFromApiUrl } from '@/utils/jms'
import deepmerge from 'deepmerge'
import { mapGetters } from 'vuex'
import IBox from '@/components/Common/IBox/index.vue'
@@ -42,8 +42,8 @@ import TableAction from './TableAction/index.vue'
import Emitter from '@/mixins/emitter'
import AutoDataTable from '../AutoDataTable/index.vue'
import QuickFilter from './TableAction/QuickFilter.vue'
import { getDayEnd, getDaysAgo } from '@/utils/common/time'
import { ObjectLocalStorage } from '@/utils/common/index'
import { getDayEnd, getDaysAgo } from '@/utils/time'
import { ObjectLocalStorage } from '@/utils/common'
export default {
name: 'ListTable',
@@ -85,13 +85,10 @@ export default {
date_from: getDaysAgo(7).toISOString(),
date_to: this.$moment(getDayEnd()).add(1, 'day').toISOString()
}
this.headerActions.datePicker = Object.assign(
{
dateStart: extraQuery.date_from,
dateEnd: extraQuery.date_to
},
this.headerActions.datePicker
)
this.headerActions.datePicker = Object.assign({
dateStart: extraQuery.date_from,
dateEnd: extraQuery.date_to
}, this.headerActions.datePicker)
}
if (this.$route.query.order) {
extraQuery['order'] = this.$route.query.order
@@ -107,9 +104,7 @@ export default {
tablePath: new URL(this.tableConfig.url || '', 'http://127.0.0.1').pathname,
objStorage: new ObjectLocalStorage('filterExpand'),
iFilterExpand: null,
reloadTable: _.debounce(this._reloadTable, 300),
searchQuery: {},
filterQuery: {}
reloadTable: _.debounce(this._reloadTable, 300)
}
},
computed: {
@@ -172,7 +167,7 @@ export default {
extraQuery: this.extraQuery
})
const checkRoot = !(this.$route.meta?.disableOrgsChange === true)
const checkPermAndRoot = action => {
const checkPermAndRoot = (action) => {
if (!this.hasActionPerm(action)) {
return this.$t('NoPermission')
}
@@ -303,26 +298,19 @@ export default {
this.$delete(this.extraQuery, key)
}
},
getMergedQuery() {
return { ...this.searchQuery, ...this.filterQuery }
},
search(attrs) {
const init = this.updateInitQuery(attrs)
if (init) {
return
}
this.searchQuery = attrs
const merged = this.getMergedQuery()
this.$log.debug('ListTable: search table', attrs)
this.$emit('TagSearch', attrs)
this.$refs.dataTable?.$refs.dataTable?.search(merged, true)
this.$refs.dataTable?.$refs.dataTable?.search(attrs, true)
},
filter(attrs) {
this.filterQuery = attrs
const merged = this.getMergedQuery()
this.$emit('TagFilter', attrs)
this.$log.debug('ListTable: found filter change', attrs)
this.$refs.dataTable?.$refs.dataTable?.search(merged, true)
this.search(attrs)
},
hasActionPerm(action) {
const permRequired = this.permissions[action]
@@ -393,6 +381,6 @@ export default {
//修改颜色
.el-button--text {
color: #409eff;
color: #409EFF;
}
</style>

Some files were not shown because too many files have changed in this diff Show More