Compare commits

...

139 Commits

Author SHA1 Message Date
Bai
65392f1d0c perf: ZTree Search Input support select node or asset 2025-12-29 18:09:04 +08:00
Bai
777b139fde perf: modify create asset field nodes to node; modify user-detail-tree and workbench-tree 2025-12-26 18:03:39 +08:00
Bai
cccd2cbb7a perf: ztree filterTree, add shouldCollapseNodes 2025-12-24 16:48:24 +08:00
Bai
f75a4007f6 perf: ztree filterTree, default expand matchNodes parents, not include childrens 2025-12-24 16:31:33 +08:00
Bai
a9c1e06706 refactor: while search tree, re-initial ZTree use API data. 2025-12-23 18:47:52 +08:00
Bai
b66b779e79 perf: modify AssetTreeTable to async:false, and fix sync filter tree nodes expand all children 2025-12-22 18:50:23 +08:00
feng
841ba521f6 perf: assistant content 2025-12-12 10:17:18 +08:00
feng
d2feaa021e perf: Chat input tool switch css 2025-12-11 15:24:13 +08:00
feng
c231f66e4a perf: remove typeOptions 2025-12-10 18:36:42 +08:00
feng
7fe6ab5f1b perf: mcp tools 2025-12-10 18:17:38 +08:00
feng
c037b52bb6 perf: tools 2025-12-10 17:11:07 +08:00
feng
37a45cbc6e perf: thinking 2025-12-10 17:11:07 +08:00
github-actions[bot]
3d90d56373 perf: Update Dockerfile with new base image tag 2025-12-10 17:11:07 +08:00
feng
587f49286f perf: To kael 2025-12-10 17:11:07 +08:00
feng
5e1b32aaf9 perf: Open web ui settigns 2025-12-10 17:11:07 +08:00
feng
e1bc8245c9 fix: Ticket comment not display 2025-12-10 17:11:07 +08:00
w940853815
6b6de2b2c5 perf: Update asset detection logic in AccountRiskList for improved menu display 2025-12-10 17:11:07 +08:00
feng
7913979b4b perf: Account bulk error prompt 2025-12-10 17:11:07 +08:00
w940853815
16d8c7c9ac perf: Enhance error message handling for nested plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
zhaojisen
768210ef86 Fixed: Fix the issue where the global organization text is not displayed completely in the organization dropdown. 2025-12-10 17:11:07 +08:00
w940853815
ce9d598683 fix: Ensure progress display is shown correctly during job execution 2025-12-10 17:11:07 +08:00
zhaojisen
2d75514aa1 Fixed: Fix the issue where creating an SSH public key for a third-party user redirects to the login password tab. 2025-12-10 17:11:07 +08:00
w940853815
669831c533 fix: Click Review to enter comments, and the page will be dimmed 2025-12-10 17:11:07 +08:00
w940853815
eb3f3ba441 perf: Add support for error message extraction from plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
feng
46cf67e10f fix: Account push show params 2025-12-10 17:11:07 +08:00
w940853815
4e0b20b8e8 perf: Add condition to check node type in AccountRiskList for asset detection 2025-12-10 17:11:07 +08:00
w940853815
a6faad4b93 perf: Refactor error handling and improve parameter assignment in job forms 2025-12-10 17:11:07 +08:00
w940853815
cb51ccae61 perf: Improve error message formatting for array of strings 2025-12-10 17:11:07 +08:00
w940853815
5a9d94aad0 perf: Enhance AccountFormatter with options to exclude accounts 2025-12-10 17:11:07 +08:00
w940853815
69bdc7c0a2 perf: Job execution detail content add text-overflow class 2025-12-10 17:11:07 +08:00
w940853815
2441c08da5 perf: Support batch import of leak passwords 2025-12-10 17:11:07 +08:00
ibuler
f9c244f006 perf: small size some png 2025-12-10 17:11:07 +08:00
fit2bot
9c5ff2b3a8 perf: upgrade vue version (#5260)
* perf: upgrade vue version

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-10 17:11:07 +08:00
w940853815
91bfb4d723 perf: Add confirmation dialog before syncing platform protocols 2025-12-10 17:11:07 +08:00
feng
8d69418613 perf: Bulk account support node 2025-10-31 17:19:11 +08:00
w940853815
2d798053b3 perf: Translate select assets 2025-10-31 10:35:56 +08:00
ibuler
e48385c70e perf: revert keyword to search 2025-10-30 16:27:40 +08:00
fit2bot
4ed529bbfb perf: remove sub mod (#5254)
* perf: remove sub mod

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-30 15:23:58 +08:00
fit2bot
ca5350cc96 perf: update lodash version 2025-10-30 14:09:50 +08:00
w940853815
9046245cdb perf: Add header to file uploader with clear selection 2025-10-28 18:50:07 +08:00
fit2bot
991a512e85 perf: Optimize form uniqueness validation (#5250)
* perf: Optimize form uniqueness validation

* perf: Add some comments

* perf: check logical

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
2025-10-28 16:41:49 +08:00
dependabot[bot]
6c008e3879 chore(deps): bump dompurify from 3.1.6 to 3.2.4
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.1.6 to 3.2.4.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.1.6...3.2.4)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.2.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 15:25:17 +08:00
zhaojisen
d16fbc3b13 Fixed: Fix ESLint errors 2025-10-27 14:38:12 +08:00
fit2bot
a0354e30c7 perf: org select style 2025-10-27 12:04:31 +08:00
mikebofs
0661bb0ea0 perf: all assets can add to zone 2025-10-24 10:39:09 +08:00
ibuler
e72fe04525 perf: add stat load helptip 2025-10-24 10:38:30 +08:00
feng
59cec3d6a9 perf: getDrawerTitle 2025-10-24 10:36:58 +08:00
w940853815
829f4ceaa4 fix: Replace alert with console.log for error handling 2025-10-22 18:22:51 +08:00
w940853815
b615e35e49 fix: Update help message rendering to use v-html for proper display 2025-10-22 14:59:55 +08:00
w940853815
cda282ac6b fix: Details page does not display labels. 2025-10-20 10:35:02 +08:00
w940853815
f2d44a2fd1 perf: Enhance email report success feedback with response message 2025-10-16 11:24:58 +08:00
w940853815
52f3ba012b perf: Hide vertical overflow in terminal display for improved UI 2025-10-16 10:11:32 +08:00
Chenyang Shen
d43e6a19bf Merge pull request #5236 from jumpserver/pr@dev@feat_add_tip_for_data_masking_form
feat: add fields tip for data masking form
2025-10-15 19:35:48 +08:00
Aaron3S
1f628e0d40 feat: add fields tip for data masking form 2025-10-15 19:33:47 +08:00
Chenyang Shen
9922a495eb Merge pull request #5235 from jumpserver/pr@dev@feat_update_column_show
feat: update data masking column show
2025-10-15 16:52:14 +08:00
Aaron3S
ae7549a00d feat: update data masking column show 2025-10-15 16:50:34 +08:00
Chenyang Shen
a5e870035e Merge pull request #5234 from jumpserver/pr@dev@feat_data_masking_license
feat: datamasking require license
2025-10-15 16:17:20 +08:00
Aaron3S
38f1ab3075 feat: datamasking require license 2025-10-15 16:15:10 +08:00
feng
c05248a1ab perf: Del remote account 2025-10-15 15:22:58 +08:00
w940853815
efacae517a perf: Add success message on successful update of system message subscription 2025-10-15 10:16:34 +08:00
Chenyang Shen
70b71a44d3 Merge pull request #5230 from jumpserver/pr@dev@feat_data_masking_detail
feat: add detail for data masking
2025-10-14 18:04:03 +08:00
Aaron3S
0ac220341e feat: add detail for data masking 2025-10-14 18:02:21 +08:00
w940853815
fc8fd2c8eb perf:Risk detection: When operating assets in batch, there is no prompt that the task is running 2025-10-14 16:49:39 +08:00
Chenyang Shen
f575eaafb6 Merge pull request #5228 from jumpserver/pr@dev@feat_change_some_translate
feat: change some translate
2025-10-14 16:05:06 +08:00
Aaron3S
2f79134023 feat: change some translate 2025-10-14 15:58:19 +08:00
feng
c59e6268b3 perf: remve chat fa-arrows-alt 2025-10-14 14:54:26 +08:00
w940853815
3344e01a9c perf: Asset selection optimization 2025-10-13 15:58:21 +08:00
zhaojisen
0a42031220 Fixed: Fix the issue where updating the SSH public key incorrectly redirects to a page. 2025-10-13 11:21:48 +08:00
feng
686e48f273 perf: Translate 2025-10-11 15:22:12 +08:00
w940853815
f16830adfe fix: In rule selection, the global organization will display all assets and users. 2025-10-11 10:52:51 +08:00
Chenyang Shen
be8d09b777 Merge pull request #5221 from jumpserver/pr@dev@feat_update_menu_name
feat: update menu name
2025-10-11 00:03:48 +08:00
Aaron3S
5ed2b6d9c8 feat: update menu name 2025-10-11 00:01:06 +08:00
feng
ece3ebc6e9 perf: Global can update and create ssh key 2025-10-09 18:01:55 +08:00
Aaron3S
1afd8dc934 feat: data masking 2025-10-09 10:39:05 +08:00
feng
a239060798 perf: Open web ui 2025-09-25 15:53:35 +08:00
w940853815
05032d6c78 perf: Remove useless code 2025-09-25 15:34:35 +08:00
w940853815
9dc35a603e perf: File transfer select asset component replacement 2025-09-25 15:34:35 +08:00
w940853815
43448aa482 perf: Style adjustment 2025-09-25 15:34:35 +08:00
w940853815
8887e98249 perf: Group platform 2025-09-25 15:34:35 +08:00
w940853815
43e4dcd760 perf: User checkbox select 2025-09-25 15:34:35 +08:00
w940853815
94d4be7555 perf: User checkbox select 2025-09-25 15:34:35 +08:00
w940853815
8210e2810f perf: Optimize the asset selection method of Adhoc 2025-09-25 15:34:35 +08:00
w940853815
05be6876f3 perf: Optimize file upload progress bar display 2025-09-22 17:39:01 +08:00
zhaojisen
38f8d88cdf Fixed: Fix the issue of incorrect form error messages 2025-09-22 14:14:43 +08:00
feng
3b0def17d7 perf: Translate 2025-09-22 11:21:24 +08:00
w940853815
57b0fa0b0e perf: Community edition hide report export button 2025-09-19 15:34:53 +08:00
w940853815
9611eb9c73 fix: Improve chart management by using a global _echarts object for tracking active charts 2025-09-17 19:02:24 +08:00
w940853815
a2bbb6555b fix: Optimize data fetching in AccountSummary and UserAssetActivity components 2025-09-17 18:01:03 +08:00
zhaojisen
ddd25208d8 Fixed: Fix the issue where error message prompts are being obscured. 2025-09-17 17:48:08 +08:00
w940853815
fe8db5831b fix: Edit template onPerformError error 2025-09-17 16:26:54 +08:00
feng
0c29ade399 perf: Compatible v3 ssh_key_change_strategy 2025-09-17 14:32:40 +08:00
zhaojisen
f8b2840d89 Fixed: Restore component update component settings logic 2025-09-17 14:11:40 +08:00
zhaojisen
52a18c3c35 Fixed: Fix the error reporting issue in risk detection review 2025-09-17 10:35:49 +08:00
w940853815
ace9cdcd68 perf: Translate msg template 2025-09-16 19:14:22 +08:00
w940853815
7098c2266e fix: Copy name field failed 2025-09-16 17:56:11 +08:00
w940853815
58d3489c33 fix: There is something wrong with the format of the site message 2025-09-16 17:56:11 +08:00
w940853815
396d20f31e fix: Remove priority fields in ACL components 2025-09-16 17:46:21 +08:00
zhaojisen
c8b866412a Fixed: Restore the logic for adding gateway lists 2025-09-16 17:33:13 +08:00
feng
63b163e382 perf: Translate 2025-09-16 16:14:46 +08:00
zhaojisen
e1acc642ca perf: Optimize the way connection methods are displayed in the table. 2025-09-16 11:35:45 +08:00
w940853815
e3f4cc68d2 fix: Export report pdf failed 2025-09-15 18:48:24 +08:00
w940853815
983aff62f2 fix: Update account existence check 2025-09-15 17:52:26 +08:00
zhaojisen
26e1ffdbdd Fixed: Fix Dashboard export issue on the Audit Workbench 2025-09-15 17:25:51 +08:00
zhaojisen
2002519f30 Fixed: Fix the issue of storage failure when updating components. 2025-09-15 14:40:58 +08:00
zhaojisen
c8f3e71d2c Fixed: Fixed the issue of failing to add a gateway in domain details. 2025-09-15 14:22:30 +08:00
w940853815
bd8f21e17a perf: Init template name 2025-09-12 18:32:13 +08:00
w940853815
0981b1854b perf: Remember select template 2025-09-12 18:32:13 +08:00
w940853815
94ab823b50 fix: Trim whitespace in email template content 2025-09-12 18:32:13 +08:00
w940853815
b378f41c07 fix: vue el-autocomplete flashes 2025-09-12 16:39:35 +08:00
zhaojisen
6cce077441 Confirm dialog for new announcement content 2025-09-11 17:11:40 +08:00
ibuler
361e522c5e perf: tag input min width 2025-09-11 16:53:26 +08:00
w940853815
08161c892e fix: LineChart component error 2025-09-11 15:09:05 +08:00
ibuler
39e4fdf40c perf: 优化 action btn 的大小 2025-09-11 15:03:37 +08:00
w940853815
11011f6b68 fix: Modify Dialog components import 2025-09-11 14:51:34 +08:00
mikebofs
7eb2e08f03 perf: add search place holder 2025-09-10 18:18:47 +08:00
w940853815
7d422bea51 perf: And license validation in msg template page 2025-09-10 17:24:58 +08:00
feng
3367ec624c perf: Ticket not display comment 2025-09-10 17:07:55 +08:00
fit2bot
091db8e6aa feat: setting email template content (#5173)
* feat: setting email template content

* perf: template list

* perf: show variable help text

* perf: custom template render to string

* perf: help text

* perf: update email template variable structure and improve formatting

* perf: Template content reset

* perf: typo MsgTemplate

* fix: email page error

* fix: package.json conflict

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: w940853815 <940853815@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-09-10 16:49:14 +08:00
feng
5d0f2c5c60 perf: Message zIndex 20000 2025-09-09 15:09:20 +08:00
feng
71b9e87786 perf: Update mysql db_name not required 2025-09-09 11:09:14 +08:00
fit2bot
ae0b3572f8 perf: Complete the search (#5175)
* perf: global search

* perf: Update Dockerfile with new base image tag

* perf: 完成基本搜索

* perf: show search direct

* perf: change search style

* perf: 优化 panel 格式,不再用 select

* perf: search add route

* perf: add route search

* perf: change view

* perf: add to body

* perf: 再次优化格式

* perf: 优化了一些显示

* perf: 基本完成搜索功能

* perf: 完成搜索

* perf: Update Dockerfile with new base image tag

* perf: merge with remote

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: ibuler <ibuler@qq.com>
2025-09-05 17:23:37 +08:00
fit2bot
217a09ebd6 perf: global search (#5171)
* perf: global search

* perf: Update Dockerfile with new base image tag

* perf: 完成基本搜索

* perf: show search direct

* perf: change search style

* perf: 优化 panel 格式,不再用 select

* perf: search add route

* perf: add route search

* perf: change view

---------

Co-authored-by: ibuler <ibuler@qq.com>
2025-09-05 16:39:37 +08:00
w940853815
ca61a75997 perf: refine layout of card body to avoid affecting el-progress 2025-09-01 10:09:19 +08:00
ibuler
3310459b2c perf: reports menus 2025-08-28 15:08:20 +08:00
w940853815
b000ca46c9 perf: update event handler from rendered to finished in Echart component 2025-08-26 16:23:23 +08:00
mikebofs
36bdf6db2b perf: echarts to components 2025-08-26 16:23:23 +08:00
mikebofs
5f20a79c0a perf: support exclude some account on permission 2025-08-26 14:58:24 +08:00
zhaojisen
67eee7bb45 Fixed: Fix the issue where the number of exported items does not match the number of items selected. 2025-08-25 16:45:17 +08:00
feng
4b54c07a42 perf: Report menu perm 2025-08-22 18:51:32 +08:00
feng626
c907e158eb Merge pull request #5152 from jumpserver/pr@dev@dashboard_export
perf: export dashboard
2025-08-22 17:57:54 +08:00
feng626
7a7d41803c Merge branch 'dev' into pr@dev@dashboard_export 2025-08-22 17:57:19 +08:00
feng
b9604a6d02 perf: report perm 2025-08-22 17:50:32 +08:00
mikebofs
794b612b8b perf: change echarts define 2025-08-22 15:26:56 +08:00
mikebofs
8a965daa39 perf: remove line chart 2025-08-22 15:15:14 +08:00
mikebofs
c0705e72e3 perf: export dashboard 2025-08-22 14:42:04 +08:00
wrd
d755fd37bd fix: Account automation report show error 2025-08-21 22:18:36 +08:00
mikebofs
ed8e6b479f stash it 2025-08-21 18:25:18 +08:00
mikebofs
c42e922d36 merge: with remote 2025-08-21 16:30:29 +08:00
mikebofs
fd7e32bbbd perf: export dashboard 2025-08-21 15:30:37 +08:00
220 changed files with 11188 additions and 5411 deletions

View File

@@ -294,7 +294,8 @@ module.exports = {
],
skipIfMatch: [
'http://[^s]*',
'^[-\\w]+/[-\\w\\.]+$' // For import paths
'^[-\\w]+/[-\\w\\.]+$',
String.raw`^\/api\/[a-z0-9\/._-]+$`,
],
minLength: 3
}

57
.prettierignore Normal file
View File

@@ -0,0 +1,57 @@
# Dependencies
node_modules/
dist/
build/
lina/
# Logs
*.log
logs/
# Runtime data
pids/
*.pid
*.seed
# Coverage directory used by tools like istanbul
coverage/
# Generated files
*.min.js
*.min.css
# Package files
*.tgz
*.tar.gz
# Lock files
package-lock.json
yarn.lock
# Build outputs
*.map
# Config files that shouldn't be formatted
.eslintrc.js
babel.config.js
jest.config.js
vue.config.js
postcss.config.js
# Theme files
src/styles/fonts/
public/fonts/
lina/fonts/
# Assets
src/assets/
public/
# Mock data
mock/
# Test files
tests/
# Documentation
*.md

View File

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

View File

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

76
PRETTIER.md Normal file
View File

@@ -0,0 +1,76 @@
# Prettier 配置说明
本项目已配置 Prettier 代码格式化工具,**仅在保存时自动格式化**,不进行批量格式化,以保持现有代码风格。
## 配置文件
- `.prettierrc` - Prettier 配置文件
- `.prettierignore` - 忽略格式化的文件列表
- `.vscode/settings.json` - VSCode 编辑器配置(保存时自动格式化)
- `.vscode/extensions.json` - 推荐的 VSCode 扩展
## 使用方法
### 1. 安装依赖
项目已安装以下依赖:
- `prettier@^2.8.8` - Prettier 核心
- `eslint-plugin-prettier@^3.1.4` - ESLint 与 Prettier 集成
- `eslint-config-prettier@^6.15.0` - 禁用与 Prettier 冲突的 ESLint 规则
### 2. 命令行使用
```bash
# ESLint 检查和修复
npm run fix
```
**注意**:本项目配置为仅在保存时自动格式化,不提供批量格式化命令。
### 3. VSCode 编辑器配置
确保安装了推荐的扩展:
- Prettier - Code formatter (esbenp.prettier-vscode)
- ESLint (dbaeumer.vscode-eslint)
- Vetur (octref.vetur)
配置已设置为保存时自动格式化。
### 4. Git 提交钩子
项目使用 `husky``lint-staged` 在提交时进行代码检查:
- 提交时运行 ESLint 检查和修复
- 不进行批量格式化,保持原有代码风格
## Prettier 配置说明
```json
{
"semi": false, // 不使用分号
"singleQuote": true, // 使用单引号
"tabWidth": 0, // 不使用缩进
"useTabs": false, // 使用空格而不是制表符
"trailingComma": "none", // 不使用尾随逗号
"printWidth": 100, // 行宽 100 字符
"bracketSpacing": true, // 对象括号内有空格
"arrowParens": "avoid", // 箭头函数单参数时不使用括号
"endOfLine": "lf", // 使用 LF 换行符
"vueIndentScriptAndStyle": false // Vue 文件中 script 和 style 标签不缩进
}
```
## 常见问题
### Q: 如何临时禁用格式化?
A: 使用注释:
```javascript
// prettier-ignore
const uglyCode = {
a:1,b:2
}
```
### Q: 如何添加文件到忽略列表?
A: 编辑 `.prettierignore` 文件,添加文件或目录路径。
### Q: VSCode 保存时没有自动格式化?
A: 检查是否安装了 Prettier 扩展,并确认 `.vscode/settings.json` 配置正确。

View File

@@ -1,150 +1,147 @@
{
"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",
"element-ui": "^2.15.14",
"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-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",
"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"
"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"
}

View File

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

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: 55 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -15,10 +15,25 @@ export const accountFieldsMeta = (vm) => {
}
return {
nodes: {
component: Select2,
label: vm.$t('Node'),
el: {
value: [],
ajax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
}
},
hidden: () => {
return !vm.addTemplate
}
},
assets: {
component: AssetSelect,
label: vm.$t('Asset'),
rules: [Required],
el: {
multiple: false
},
@@ -33,7 +48,7 @@ export const accountFieldsMeta = (vm) => {
get disabled() {
return vm.isDisabled
},
multiple: false,
multiple: vm.addTemplate,
ajax: {
url: '/api/v1/accounts/account-templates/',
transformOption: (item) => {

View File

@@ -63,7 +63,7 @@ export default {
encryptedFields: ['secret'],
fields: [
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
[this.$t('Asset'), ['assets']],
[this.$t('Asset'), ['nodes', 'assets']],
[this.$t('Secret'), [
'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key',

View File

@@ -93,8 +93,8 @@ export default {
iVisible = true
data = formValue
url = `/api/v1/accounts/accounts/bulk/`
if (data.assets.length === 0) {
this.$message.error(this.$tc('PleaseSelectAsset'))
if ((!data.assets || data.assets.length === 0) && (!data.nodes || data.nodes.length === 0)) {
this.$message.error(this.$tc('PleaseSelectAssetOrNode'))
return
}
}
@@ -107,6 +107,10 @@ export default {
this.$emit('add', true)
}
}).catch(error => {
if (error?.response?.data?.code === 'no_valid_assets') {
this.$message.error(error?.response?.data?.detail)
return
}
this.iVisible = true
this.handleResult(null, error)
})

View File

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

View File

@@ -1,12 +1,60 @@
<template>
<div class="container">
<div v-if="hasPrompt" class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
v-bind="select"
@change="onSelectChange"
/>
<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>
<div class="chat-input">
<el-input
@@ -37,9 +85,37 @@ export default {
type: Boolean,
default: false
},
hasPrompt: {
modelOptions: {
type: Array,
default: () => []
},
selectedModel: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: true
default: false
},
toolOptions: {
type: Array,
default: () => []
},
toolServerOptions: {
type: Array,
default: () => []
},
selectedTools: {
type: Array,
default: () => []
},
selectedToolServers: {
type: Array,
default: () => []
},
toolsLoading: {
type: Boolean,
default: false
}
},
data() {
@@ -47,15 +123,10 @@ export default {
isIM: false,
inputValue: '',
select: {
url: '/api/v1/settings/chatai-prompts/',
value: '',
multiple: false,
placeholder: this.$t('Role'),
ajax: {
transformOption: (item) => {
return { label: item.name, value: item.content }
}
}
placeholder: this.$t('Model'),
options: []
}
}
},
@@ -64,7 +135,32 @@ export default {
isLoading: state => state.chat.loading
}),
isSelectDisabled() {
return !!this.select.value
return false
},
options() {
return (this.modelOptions || []).map(item => {
return { label: item.name || item.id, value: item.id }
})
},
selectedToolsSet() {
return new Set(this.selectedTools || [])
},
selectedToolServersSet() {
return new Set(this.selectedToolServers || [])
}
},
watch: {
modelOptions: {
immediate: true,
handler(val) {
this.select.options = (val || []).map(item => ({ label: item.name || item.id, value: item.id }))
}
},
selectedModel: {
immediate: true,
handler(val) {
this.select.value = val || ''
}
}
},
methods: {
@@ -84,7 +180,25 @@ export default {
this.inputValue = ''
},
onSelectChange(value) {
this.$emit('select-prompt', value)
this.$emit('select-model', value)
},
toggleTool(id) {
const set = new Set(this.selectedTools || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tools', Array.from(set))
},
toggleToolServer(id) {
const set = new Set(this.selectedToolServers || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tool-servers', Array.from(set))
}
}
}
@@ -99,9 +213,18 @@ export default {
.chat-action {
width: 100%;
margin: 6px 0;
display: flex;
align-items: center;
gap: 8px;
.model-select {
flex: 0 0 48%;
max-width: 240px;
min-width: 160px;
}
&::v-deep .el-select {
width: 50%;
width: 100%;
.el-input__inner {
height: 28px;

View File

@@ -3,9 +3,13 @@
<div class="chart-item-container">
<div class="avatar">
<el-avatar
:src="isUserRole ? userUrl : chatUrl"
v-if="isUserRole"
:src="userUrl"
class="header-avatar"
/>
<el-avatar v-else class="header-avatar" :style="{ backgroundColor: 'transparent' }">
<ModelIcon :name="modelIconName" class-name="model-icon" />
</el-avatar>
</div>
<div class="content">
<div class="operational">
@@ -76,6 +80,7 @@
<script>
import MessageText from './MessageText.vue'
import ModelIcon from '../../models/ModelIcon.vue'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common/index'
import { useChat } from '../../useChat.js'
@@ -85,7 +90,8 @@ const { setLoading, removeLoadingMessageInChat } = useChat()
export default {
components: {
MessageText
MessageText,
ModelIcon
},
props: {
item: {
@@ -93,6 +99,10 @@ export default {
default: () => {
}
},
selectedModel: {
type: String,
default: ''
},
isTerminal: {
type: Boolean,
default: false
@@ -129,10 +139,8 @@ export default {
? this.$i18n.t('ServerBusyRetry')
: ''
},
chatUrl() {
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
? require('@/assets/img/chat.png')
: require('@/assets/img/deepSeek.png')
modelIconName() {
return (this.item?.message?.model || this.selectedModel || this.publicSettings.CHAT_AI_TYPE || '').toString()
}
},
methods: {
@@ -171,11 +179,18 @@ export default {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: transparent;
&::v-deep img {
background-color: #fff;
}
}
.model-icon {
width: 100%;
height: 100%;
display: block;
}
}
.content {

View File

@@ -40,7 +40,7 @@ export default {
text() {
const value = this.message?.content || ''
if (value && this.markdown) {
return this.markdown?.render(value)
return this.renderContentWithDetails(value)
}
return this.$xss.process(value)
}
@@ -120,6 +120,77 @@ 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) => {
@@ -258,4 +329,64 @@ export default {
.loading-box span:nth-child(3) {
animation-delay: 0.49s;
}
.kael-detail {
margin: 8px 0;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #f7f8fa;
&__header {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #6b7280;
}
&__title {
font-weight: 600;
}
&__status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #10b981;
}
&__body {
margin-top: 6px;
padding-left: 8px;
border-left: 2px solid #e5e5e5;
}
&--reasoning.is-pending {
border-color: #f59e0b40;
background: #fff8e6;
.kael-detail__status-dot {
background: #f59e0b;
animation: kael-pulse 1.2s ease-in-out infinite;
}
}
&--reasoning.is-done {
border-color: #dbeafe;
background: #f4f6ff;
}
}
@keyframes kael-pulse {
0% {
opacity: 0.45;
}
50% {
opacity: 1;
}
100% {
opacity: 0.45;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,8 @@
</template>
<script>
import { BASE_URL } from '@/utils/common/index'
export default {
props: {
active: {
@@ -41,6 +43,16 @@ export default {
},
handleExpand() {
this.$emit('expand-full')
},
async openWebsite() {
let url = `${BASE_URL}/?_=${Date.now()}`
if (process.env.NODE_ENV !== 'production') {
url = url.replace('9528', '5173')
}
const newUrl = new URL(url)
window.open(newUrl.toString(), '_blank')
return url
}
}
}

View File

@@ -46,7 +46,6 @@
import Sidebar from './components/Sidebar/index.vue'
import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/request'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
import { mapGetters } from 'vuex'
@@ -82,7 +81,8 @@ export default {
height: '400px',
expanded: false,
clientOffset: {},
currentTerminalContent: {}
currentTerminalContent: {},
initialized: false
}
},
computed: {
@@ -123,11 +123,18 @@ export default {
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.initWebSocket()
this.initAssistant()
return
}
const msg = event.data
@@ -152,11 +159,6 @@ export default {
}
this.$refs.drawer.handleHeaderMoveUp(event)
},
initWebSocket() {
if (!ws) {
this.$refs.component?.init()
}
},
onClose() {
this.$refs.drawer.show = false
},
@@ -170,7 +172,6 @@ export default {
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
@@ -184,8 +185,8 @@ export default {
})
},
onToggle(status) {
this.initWebSocket()
if (status) {
this.initAssistant()
getInputFocus()
}
}

View File

@@ -0,0 +1,33 @@
<template>
<svg
:stroke-width="strokeWidth"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
:class="className"
>
<path
fill="#6b7280"
style="fill: #6b7280 !important"
d="M11.2475 18.25C10.6975 18.25 10.175 18.1455 9.67999 17.9365C9.18499 17.7275 8.74499 17.436 8.35999 17.062C7.94199 17.205 7.50749 17.2765 7.05649 17.2765C6.31949 17.2765 5.63749 17.095 5.01049 16.732C4.38349 16.369 3.87749 15.874 3.49249 15.247C3.11849 14.62 2.93149 13.9215 2.93149 13.1515C2.93149 12.8325 2.97549 12.486 3.06349 12.112C2.62349 11.705 2.28249 11.2375 2.04049 10.7095C1.79849 10.1705 1.67749 9.6095 1.67749 9.0265C1.67749 8.4325 1.80399 7.8605 2.05699 7.3105C2.30999 6.7605 2.66199 6.2875 3.11299 5.8915C3.57499 5.4845 4.10849 5.204 4.71349 5.05C4.83449 4.423 5.08749 3.862 5.47249 3.367C5.86849 2.861 6.35249 2.465 6.92449 2.179C7.49649 1.893 8.10699 1.75 8.75599 1.75C9.30599 1.75 9.82849 1.8545 10.3235 2.0635C10.8185 2.2725 11.2585 2.564 11.6435 2.938C12.0615 2.795 12.496 2.7235 12.947 2.7235C13.684 2.7235 14.366 2.905 14.993 3.268C15.62 3.631 16.1205 4.126 16.4945 4.753C16.8795 5.38 17.072 6.0785 17.072 6.8485C17.072 7.1675 17.028 7.514 16.94 7.888C17.38 8.295 17.721 8.768 17.963 9.307C18.205 9.835 18.326 10.3905 18.326 10.9735C18.326 11.5675 18.1995 12.1395 17.9465 12.6895C17.6935 13.2395 17.336 13.718 16.874 14.125C16.423 14.521 15.895 14.796 15.29 14.95C15.169 15.577 14.9105 16.138 14.5145 16.633C14.1295 17.139 13.651 17.535 13.079 17.821C12.507 18.107 11.8965 18.25 11.2475 18.25ZM7.17199 16.1875C7.72199 16.1875 8.20049 16.072 8.60749 15.841L11.7095 14.059C11.8195 13.982 11.8745 13.8775 11.8745 13.7455V12.3265L7.88149 14.62C7.63949 14.763 7.39749 14.763 7.15549 14.62L4.03699 12.8215C4.03699 12.8545 4.03149 12.893 4.02049 12.937C4.02049 12.981 4.02049 13.047 4.02049 13.135C4.02049 13.696 4.15249 14.213 4.41649 14.686C4.69149 15.148 5.07099 15.511 5.55499 15.775C6.03899 16.05 6.57799 16.1875 7.17199 16.1875ZM7.33699 13.498C7.40299 13.531 7.46349 13.5475 7.51849 13.5475C7.57349 13.5475 7.62849 13.531 7.68349 13.498L8.92099 12.7885L4.94449 10.4785C4.70249 10.3355 4.58149 10.121 4.58149 9.835V6.2545C4.03149 6.4965 3.59149 6.8705 3.26149 7.3765C2.93149 7.8715 2.76649 8.4215 2.76649 9.0265C2.76649 9.5655 2.90399 10.0825 3.17899 10.5775C3.45399 11.0725 3.81149 11.4465 4.25149 11.6995L7.33699 13.498ZM11.2475 17.161C11.8305 17.161 12.3585 17.029 12.8315 16.765C13.3045 16.501 13.6785 16.138 13.9535 15.676C14.2285 15.214 14.366 14.697 14.366 14.125V10.561C14.366 10.429 14.311 10.33 14.201 10.264L12.947 9.538V14.1415C12.947 14.4275 12.826 14.642 12.584 14.785L9.46549 16.5835C10.0045 16.9685 10.5985 17.161 11.2475 17.161ZM11.8745 11.122V8.878L10.01 7.822L8.12899 8.878V11.122L10.01 12.178L11.8745 11.122ZM7.05649 5.8585C7.05649 5.5725 7.17749 5.358 7.41949 5.215L10.538 3.4165C9.99899 3.0315 9.40499 2.839 8.75599 2.839C8.17299 2.839 7.64499 2.971 7.17199 3.235C6.69899 3.499 6.32499 3.862 6.04999 4.324C5.78599 4.786 5.65399 5.303 5.65399 5.875V9.4225C5.65399 9.5545 5.70899 9.659 5.81899 9.736L7.05649 10.462V5.8585ZM15.4385 13.7455C15.9885 13.5035 16.423 13.1295 16.742 12.6235C17.072 12.1175 17.237 11.5675 17.237 10.9735C17.237 10.4345 17.0995 9.9175 16.8245 9.4225C16.5495 8.9275 16.192 8.5535 15.752 8.3005L12.6665 6.5185C12.6005 6.4745 12.54 6.458 12.485 6.469C12.43 6.469 12.375 6.4855 12.32 6.5185L11.0825 7.2115L15.0755 9.538C15.1965 9.604 15.2845 9.692 15.3395 9.802C15.4055 9.901 15.4385 10.022 15.4385 10.165V13.7455ZM12.122 5.3635C12.364 5.2095 12.606 5.2095 12.848 5.3635L15.983 7.195C15.983 7.118 15.983 7.019 15.983 6.898C15.983 6.37 15.851 5.8695 15.587 5.3965C15.334 4.9125 14.9655 4.5275 14.4815 4.2415C14.0085 3.9555 13.4585 3.8125 12.8315 3.8125C12.2815 3.8125 11.803 3.928 11.396 4.159L8.29399 5.941C8.18399 6.018 8.12899 6.1225 8.12899 6.2545V7.6735L12.122 5.3635Z"
/>
</svg>
</template>
<script>
export default {
name: 'ChatGPTIcon',
props: {
className: {
type: String,
default: 'size-8'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<svg
:stroke-width="strokeWidth"
viewBox="0 0 24 16"
overflow="visible"
width="20"
height="20"
:class="className"
>
<g style="transform: translateX(13px) rotateZ(0deg); transform-origin: 4.775px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M0,0 C0,0 6.1677093505859375,15.470022201538086 6.1677093505859375,15.470022201538086 C6.1677093505859375,15.470022201538086 9.550004005432129,15.470022201538086 9.550004005432129,15.470022201538086 C9.550004005432129,15.470022201538086 3.382294178009033,0 3.382294178009033,0 C3.382294178009033,0 0,0 0,0 C0,0 0,0 0,0z"
/>
</g>
<g opacity="1" style="transform: none; transform-origin: 7.935px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 7.93500280380249,3.911694288253784 7.93500280380249,3.911694288253784 C7.93500280380249,3.911694288253784 10.045400619506836,9.348296165466309 10.045400619506836,9.348296165466309 C10.045400619506836,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309z M6.166755199432373,0 C6.166755199432373,0 0,15.470022201538086 0,15.470022201538086 C0,15.470022201538086 3.4480772018432617,15.470022201538086 3.4480772018432617,15.470022201538086 C3.4480772018432617,15.470022201538086 4.709278583526611,12.22130012512207 4.709278583526611,12.22130012512207 C4.709278583526611,12.22130012512207 11.16093635559082,12.22130012512207 11.16093635559082,12.22130012512207 C11.16093635559082,12.22130012512207 12.421928405761719,15.470022201538086 12.421928405761719,15.470022201538086 C12.421928405761719,15.470022201538086 15.87000560760498,15.470022201538086 15.87000560760498,15.470022201538086 C15.87000560760498,15.470022201538086 9.703250885009766,0 9.703250885009766,0 C9.703250885009766,0 6.166755199432373,0 6.166755199432373,0 C6.166755199432373,0 6.166755199432373,0 6.166755199432373,0z"
/>
</g>
</svg>
</template>
<script>
export default {
name: 'ClaudeIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<svg
id="图层_1"
xmlns="http://www.w3.org/2000/svg"
data-name="图层 1"
viewBox="0 0 71.69 52.76"
:class="className"
:stroke-width="strokeWidth"
>
<path
id="path"
fill="#4d6bfe"
style="fill: #4d6bfe !important"
d="M523.77,276.34c-.76-.38-1.08.33-1.53.69a4,4,0,0,0-.41.41,5.07,5.07,0,0,1-4.1,1.87,8,8,0,0,0-6.46,2.53,5.82,5.82,0,0,0-3.72-4.62,6.39,6.39,0,0,1-2.85-1.94,7.76,7.76,0,0,1-.92-2.31c-.16-.48-.32-1-.87-1.05s-.83.41-1.07.82a11,11,0,0,0-1.26,5.5,11.9,11.9,0,0,0,5.49,10.14.75.75,0,0,1,.39,1c-.25.84-.54,1.65-.79,2.49-.17.53-.41.65-1,.42a16.63,16.63,0,0,1-5.18-3.52c-2.56-2.48-4.88-5.21-7.76-7.35-.68-.5-1.36-1-2.06-1.41-2.94-2.86.39-5.2,1.16-5.48s.28-1.29-2.33-1.28-5,.88-8,2a8.23,8.23,0,0,1-1.39.41,28.67,28.67,0,0,0-8.61-.3,18.57,18.57,0,0,0-13.44,7.83c-4,5.47-4.91,11.67-3.76,18.15a27.68,27.68,0,0,0,10,16.88,26.8,26.8,0,0,0,19.23,6.39c4.43-.25,9.36-.84,14.92-5.55a13.84,13.84,0,0,0,5.32,1.18,17.24,17.24,0,0,0,5.09-.38c2.2-.46,2.05-2.5,1.25-2.87-6.43-3-5-1.78-6.3-2.77,3.27-3.87,8.2-7.89,10.13-20.92a12.44,12.44,0,0,0,0-2.52c0-.51.1-.71.68-.76a12.55,12.55,0,0,0,4.62-1.42c4.17-2.28,5.85-6,6.25-10.51A1.57,1.57,0,0,0,523.77,276.34Zm-36.34,40.37c-6.24-4.9-9.27-6.52-10.52-6.45s-1,1.41-.7,2.28a8.49,8.49,0,0,0,1.11,2.21,1.14,1.14,0,0,1-.34,1.8c-2,1.24-5.5-.42-5.66-.5a26.08,26.08,0,0,1-9.87-9.88,30.15,30.15,0,0,1-3.87-13.39c-.06-1.15.28-1.56,1.42-1.77a14.31,14.31,0,0,1,4.57-.11,28.56,28.56,0,0,1,16.33,8.29,54.06,54.06,0,0,1,6.58,8.63,41.46,41.46,0,0,0,7.41,8.71,24.36,24.36,0,0,0,2.66,2C494.16,318.82,490.16,318.87,487.43,316.71Zm3-19.23a.92.92,0,0,1,.92-.92.83.83,0,0,1,.32.06.8.8,0,0,1,.34.22.9.9,0,0,1,.25.64.92.92,0,0,1-1.83,0Zm9.29,4.76a5.27,5.27,0,0,1-1.77.48,3.75,3.75,0,0,1-2.38-.76,3.57,3.57,0,0,1-1.65-2.26,5.16,5.16,0,0,1,0-1.76,2,2,0,0,0-.71-2.17,3.1,3.1,0,0,0-2.06-.59,1.63,1.63,0,0,1-.76-.24.75.75,0,0,1-.34-1.07,3.47,3.47,0,0,1,.57-.62,3.9,3.9,0,0,1,3.43,0,10,10,0,0,1,3,2.34,18.62,18.62,0,0,1,2,2.73,10.9,10.9,0,0,1,1.33,2.53C500.65,301.47,500.4,302,499.71,302.24Z"
transform="translate(-452.83 -271.91)"
/>
</svg>
</template>
<script>
export default {
name: 'DeepSeekIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="800px"
height="800px"
viewBox="0 0 512 512"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#4285f4"
style="fill: #4285f4 !important"
d="M473.16,221.48l-2.26-9.59H262.46v88.22H387c-12.93,61.4-72.93,93.72-121.94,93.72-35.66,0-73.25-15-98.13-39.11a140.08,140.08,0,0,1-41.8-98.88c0-37.16,16.7-74.33,41-98.78s61-38.13,97.49-38.13c41.79,0,71.74,22.19,82.94,32.31l62.69-62.36C390.86,72.72,340.34,32,261.6,32h0c-60.75,0-119,23.27-161.58,65.71C58,139.5,36.25,199.93,36.25,256S56.83,369.48,97.55,411.6C141.06,456.52,202.68,480,266.13,480c57.73,0,112.45-22.62,151.45-63.66,38.34-40.4,58.17-96.3,58.17-154.9C475.75,236.77,473.27,222.12,473.16,221.48Z"
/>
</svg>
</template>
<script>
export default {
name: 'GeminiIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
fill="currentColor"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#000000"
style="fill: #000000 !important"
d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"
/>
</svg>
</template>
<script>
export default {
name: 'GrokIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<component
:is="resolvedIcon"
v-if="resolvedIcon"
:class-name="className"
:stroke-width="strokeWidth"
/>
</template>
<script>
import ChatGPTIcon from './ChatGPT.vue'
import DeepSeekIcon from './DeepSeek.vue'
import GrokIcon from './Grok.vue'
import ClaudeIcon from './Claude.vue'
import GeminiIcon from './Gemini.vue'
export default {
name: 'ModelIcon',
props: {
name: {
type: String,
default: ''
},
className: {
type: String,
default: 'size-5'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
},
computed: {
normalizedName() {
return (this.name || '').toLowerCase()
},
resolvedIcon() {
const name = this.normalizedName
if (!name) return null
if (name.includes('gpt')) return ChatGPTIcon
if (name.includes('deep-seek')) return DeepSeekIcon
if (name.includes('deepseek')) return DeepSeekIcon
if (name.includes('grok')) return GrokIcon
if (name.includes('claude')) return ClaudeIcon
if (name.includes('gemini')) return GeminiIcon
return null
}
}
}
</script>

View File

@@ -11,6 +11,7 @@
import AssetTreeTable from '@/components/Apps/AssetTreeTable'
import { AccountInfoFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
import { setUrlParam } from '@/utils/common/index'
export default {
name: 'GrantedAssets',
@@ -34,7 +35,7 @@ export default {
}
const initialUrl = vm.tableConfig.initialUrl
const nodeId = node.meta.data.id
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
const url = setUrlParam(initialUrl, 'node_id', nodeId)
vm.tableConfig.url = url
}
},
@@ -70,7 +71,7 @@ export default {
showMenu: false,
showRefresh: true,
showAssets: false,
showSearch: false,
showSearch: true,
url: this.tableUrl,
// ?assets=0不显示资产. =1显示资产
treeUrl: this.treeUrl,
@@ -78,6 +79,9 @@ export default {
callback: {
onSelected: (event, node) => vm.onSelected(node, vm),
refresh: vm.refreshObjectAssetPermission
},
async: {
enable: false
}
},
tableConfig: {
@@ -109,11 +113,6 @@ export default {
},
connectivity: connectivityMeta,
comment: { ...this.comment }
},
tableAttrs: {
rowClassName({ row }) {
return !row.is_active ? 'row_disabled' : ''
}
}
},
headerActions: {

View File

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

View File

@@ -25,6 +25,7 @@ export default {
},
data() {
const [key, value] = toM2MJsonParams(this.object.assets)
const org_id = this.object.org_id || this.$store.getters.currentOrg.id
return {
config: {
headerActions: {
@@ -33,7 +34,7 @@ export default {
hasExport: false
},
tableConfig: {
url: `/api/v1/assets/assets/?${key}=${value}`,
url: `/api/v1/assets/assets/?${key}=${value}&oid=${org_id}`,
columns: ['name', 'address', 'platform', 'type', 'is_active'],
columnsShow: {
min: ['name', 'address'],

View File

@@ -25,6 +25,7 @@ export default {
},
data() {
const [key, value] = toM2MJsonParams(this.object.users)
const org_id = this.object.org_id || this.$store.getters.currentOrg.id
return {
config: {
headerActions: {
@@ -33,7 +34,7 @@ export default {
hasExport: false
},
tableConfig: {
url: `/api/v1/users/users/?${key}=${value}`,
url: `/api/v1/users/users/?${key}=${value}&oid=${org_id}`,
columns: [
'name', 'username', 'email', 'groups', 'system_roles',
'org_roles', 'source', 'is_valid'

View File

@@ -13,17 +13,12 @@
v-on="$listeners"
>
<div v-if="confirmTypeRequired === 'relogin'">
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24">
<el-alert
:title="$tc('ReLoginTitle')"
center
style="margin-bottom: 20px;"
type="error"
/>
<el-alert :title="$tc('ReLoginTitle')" center style="margin-bottom: 20px" type="error" />
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24">
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
{{ this.$t('ReLogin') }}
@@ -32,11 +27,11 @@
</el-row>
</div>
<div v-else>
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24" :span="24" class="add">
<el-select
v-model="subTypeSelected"
style="width: 100%; margin-bottom: 20px;"
style="width: 100%; margin-bottom: 20px"
@change="handleSubTypeChange"
>
<el-option
@@ -49,19 +44,22 @@
</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"
>
@@ -72,21 +70,17 @@
</el-row>
<el-row>
<el-col>
<iframe v-if="passkeyVisible" :src="passkeyUrl" style="display: none" />
<iframe
v-if="passkeyVisible"
:src="passkeyUrl"
style="display: none"
/>
<iframe
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
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)"
@@ -195,55 +189,65 @@ 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
@@ -267,23 +271,26 @@ 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()
@@ -306,16 +313,22 @@ export default {
const data = {
confirm_type: this.confirmTypeRequired,
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue
secret_key:
this.confirmTypeRequired === 'password'
? encryptPassword(this.secretValue)
: this.secretValue
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(() => {
this.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

@@ -0,0 +1,100 @@
<template>
<Dialog
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="iVisible"
class="help-dialog"
top="1vh"
width="50%"
>
<p>{{ variablesHelpText }}</p>
<table border="1" class="help-table">
<tr>
<th>{{ $tc('Variable') }}</th>
<th>{{ $tc('Description') }}</th>
<th>{{ $tc('Example') }}</th>
</tr>
<tr v-for="(item, index) in variables" :key="index">
<td :title="$tc('ClickCopy')" class="item-td text-link" @click="onCopy(item.name)">
<label class="item-label">{{ item.name }}</label>
</td>
<td><span>{{ item.label }}</span></td>
<td><span>{{ item.default }}</span></td>
</tr>
</table>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { copy } from '@/utils/common/index'
export default {
components: {
Dialog
},
props: {
visible: {
type: Boolean,
default: false
},
variables: {
type: Array,
default: () => []
},
variablesHelpText: {
type: String,
default() {
return this.$t('WatermarkVariableHelpText')
}
}
},
data() {
return {
title: this.$t('BuiltinVariable')
}
},
computed: {
iVisible: {
set(val) {
this.$emit('update:visible', val)
},
get() {
return this.visible
}
}
},
methods: {
onCopy(key) {
copy(key)
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .help-dialog.dialog .el-dialog__footer {
border-top: none;
padding: 8px;
}
.help-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #dee2e6;
}
::v-deep .help-table th,
::v-deep .help-table td {
height: 40px;
padding: 0 8px;
text-align: left;
}
::v-deep .help-table .item-td,
::v-deep .help-table .item-label {
cursor: pointer;
color: var(--color-primary);
}
</style>

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">
<template
<component
: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">
@@ -228,9 +228,9 @@ export default {
<style lang="scss" scoped>
$btn-text-color: #ffffff;
$color-btn-background: #E8F7F4;
$color-btn-focus-background: #83CBBA;
$color-divided: #E4E7ED;
$color-btn-background: #e8f7f4;
$color-btn-focus-background: #83cbba;
$color-divided: #e4e7ed;
$color-drop-menu-title: #909399;
$color-drop-menu-border: #e4e7ed;
@@ -284,6 +284,8 @@ $color-drop-menu-border: #e4e7ed;
.el-button {
padding: 2px 5px;
line-height: 1.3;
font-size: 13px;
&:not(.is-plain) {
color: $btn-text-color;
@@ -319,7 +321,6 @@ $color-drop-menu-border: #e4e7ed;
// 下拉 options
.el-dropdown-menu {
::v-deep .more-batch-processing {
&:hover {
background-color: transparent !important;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="content">
<echarts
<div>
<Echart
ref="echarts"
:options="options"
:autoresize="true"
@@ -12,9 +12,10 @@
// eslint-disable-next-line no-unused-vars
import * as echarts from 'echarts'
import { mix } from '@/utils/theme/color'
import Echart from '@/components/Dashboard/Echart.vue'
export default {
components: {},
components: { Echart },
props: {
datesMetrics: {
type: Array,

View File

@@ -0,0 +1,71 @@
<template>
<echarts
:options="iOptions"
v-bind="$attrs"
@finished="onFinished"
v-on="$listeners"
/>
</template>
<script>
import 'echarts'
export default {
components: {},
props: {
options: {
type: Object,
required: true
}
},
data() {
const urlParams = new URLSearchParams(window.location.search)
const isExport = urlParams.get('export') === 'true'
return {
isExport: isExport
}
},
computed: {
iOptions() {
return {
...this.options,
animation: !this.isExport
}
}
},
created() {
if (!window._echarts) {
window._echarts = {
total: new Set(),
finished: new Set()
}
}
// 唯一 id避免重复计数
this._chartId = `chart_${Date.now()}_${Math.random().toString(36).slice(2)}`
window._echarts.total.add(this._chartId)
},
beforeDestroy() {
if (window._echarts) {
window._echarts.total.delete(this._chartId)
window._echarts.finished.delete(this._chartId)
// 可选:当没有图表时清理全局对象
if (window._echarts.total.size === 0) {
delete window._echarts
delete window.echartsFinished
}
}
},
methods: {
onFinished() {
if (!window._echarts) return
window._echarts.finished.add(this._chartId)
if (window._echarts.finished.size === window._echarts.total.size) {
window.echartsFinished = true
}
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -1,20 +1,12 @@
<template>
<div>
<echarts
<Echart
ref="echarts"
:options="options"
:autoresize="true"
theme="light"
:class="{'disabled-when-print': !!dataUrl}"
@finished="genSnapshot"
/>
<img
v-if="dataUrl"
:src="dataUrl"
class="enabled-when-print"
style="display: none;width: 100%;"
alt="chart snapshot"
>
</div>
</template>
@@ -22,9 +14,11 @@
// eslint-disable-next-line no-unused-vars
import * as echarts from 'echarts'
import { mix } from '@/utils/theme/color'
import Echart from '@/components/Dashboard/Echart.vue'
export default {
name: 'LoginMetric',
components: { Echart },
props: {
range: {
type: String,
@@ -285,14 +279,4 @@ export default {
width: 100%;
height: 272px;
}
@media print {
.disabled-when-print {
display: none !important;
}
.enabled-when-print {
display: block !important;
width: 100% !important;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<echarts
<Echart
ref="echarts"
:options="options"
:autoresize="true"
@@ -13,8 +13,10 @@
// eslint-disable-next-line no-unused-vars
import * as echarts from 'echarts'
import { mix } from '@/utils/theme/color'
import Echart from '@/components/Dashboard/Echart.vue'
export default {
components: { Echart },
props: {
colors: {
type: Array,
@@ -35,14 +37,15 @@ export default {
}
},
data() {
return {
}
return {}
},
computed: {
options() {
const seriesList = []
const labels = this.data.map(item => item.label)
const total = _.sumBy(this.data, function(i) { return i.total })
const total = _.sumBy(this.data, function(i) {
return i.total
})
for (let i = 0, len = this.data.length; i < len; i++) {
const current = this.data[i]
let num = (current.total / total) * 100
@@ -177,8 +180,8 @@ export default {
</script>
<style lang="scss" scoped>
.echarts {
width: 100%;
height: 72px;
}
.echarts {
width: 100%;
height: 72px;
}
</style>

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>
<echarts
<Echart
ref="echarts"
:autoresize="true"
:options="options"
@@ -13,12 +13,15 @@ import 'echarts/lib/chart/line'
import 'echarts/lib/component/legend'
import Decimal from 'decimal.js'
import Echart from '@/components/Dashboard/Echart.vue'
export default {
components: { Echart },
props: {
config: {
type: Object,
default: () => {}
default: () => {
}
}
},
computed: {

View File

@@ -5,6 +5,7 @@
ref="dataForm"
:fields="totalFields"
:form="iForm"
:server-errors="serverErrors"
v-bind="$attrs"
v-on="$listeners"
>
@@ -28,6 +29,7 @@
import DataForm from '../DataForm/index.vue'
import FormGroupHeader from '@/components/Form/FormGroupHeader/index.vue'
import { FormFieldGenerator } from '@/components/Form/AutoDataForm/utils'
import { UniqueCheck } from '@/components/Form/DataForm/rules'
export default {
name: 'AutoDataForm',
@@ -65,7 +67,8 @@ export default {
totalFields: [],
loading: true,
groups: [],
errors: {}
errors: {},
serverErrors: {}
}
},
computed: {
@@ -103,6 +106,8 @@ export default {
this.generateColumns()
this.$emit('afterGenerateColumns', this.totalFields)
this.cleanFormValue()
// 初始化时清空错误
this.serverErrors = {}
this.loading = false
},
generateColumns() {
@@ -110,6 +115,47 @@ export default {
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
this.groups = generator.groups
this.$log.debug('Total fields: ', this.totalFields)
this.applyUniqueRules()
},
applyUniqueRules() {
const fields = this.totalFields || []
const currentIdGetter = () => {
return this.$route?.params?.id || this.form?.id || this.iForm?.id
}
// 移除 url 后拼接的参数
const defaultListUrl = (() => {
try {
const u = new URL(this.url, location.origin)
u.pathname = u.pathname.replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?$/, '/')
return u.origin ? u.origin + u.pathname : u.pathname
} catch (e) {
return (this.url || '').replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?($|\?)/, '/$2')
}
})()
fields.forEach(field => {
const conf = field?.uniqueCheck
if (!conf) return
const confObj = (typeof conf === 'object') ? conf : {}
const param = confObj.param || field.prop || field.id
const url = confObj.url || defaultListUrl
const label = confObj.label || field.label || param
const entityName = confObj.entityName || ''
if (!Array.isArray(field.rules)) field.rules = []
field.rules.push(UniqueCheck({
url,
param,
label,
entityName,
getIgnoreId: currentIdGetter,
fieldName: field.prop || field.id
}))
})
},
_cleanFormValue(form, remoteMeta) {
if (!form) {
@@ -139,15 +185,69 @@ export default {
cleanFormValue() {
this._cleanFormValue(this.iForm, this.remoteMeta)
},
setFieldError(name, error) {
error = error.replace(/[。.]+$/, '')
const field = this.totalFields.find((v) => v.prop === name)
if (!field) {
return
_getElFormInstance() {
try {
return this.$refs?.dataForm?.$refs?.form?.$refs?.elForm || null
} catch (e) {
return null
}
if (typeof error === 'string') {
field.el.errors = error
field.attrs.error = error
},
/**
* @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' : ''
}
}
// 不写入 totalFields避免触发 innerContent 变化导致表单值被覆盖
this.$set(this.serverErrors, name, error)
},
setErrors(errors) {
const mapped = {}
Object.entries(errors || {}).forEach(([k, v]) => {
let msg = v
console.log(k, v)
// v是数组并且数组都是字符串则拼接为字符串
if (Array.isArray(v) && v.every(item => typeof item === 'string')) msg = v.join('; ')
// 处理 [{"port":["请确保该值小于或者等于 65535。"]},{},{}] 这种情况
else if (Array.isArray(v) && v.every(item => _.isPlainObject(item))) {
const subMsg = []
v.forEach((subItem) => {
Object.values(subItem).forEach((subMsgArr) => {
if (Array.isArray(subMsgArr)) {
subMsg.push(...subMsgArr)
}
})
})
msg = subMsg.join(' ')
} else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
mapped[k] = String(msg || '')
})
this.serverErrors = mapped
const elForm = this._getElFormInstance()
if (elForm && Array.isArray(elForm.fields)) {
elForm.fields.forEach((item) => {
const msg = mapped[item.prop] || ''
item.validateMessage = msg
item.validateState = msg ? 'error' : ''
})
}
},
groupHidden(group, i) {

View File

@@ -6,6 +6,7 @@
:prop="prop"
:rules="_show && Array.isArray(data.rules) ? data.rules : []"
v-bind="data.attrs"
:error="errorText"
>
<template v-if="data.label" #label>
<span :title="data.label">
@@ -19,7 +20,8 @@
placement="right"
popper-class="help-tips"
>
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" /> <!-- Noncompliant -->
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" />
<!-- Noncompliant -->
<i class="fa fa-question-circle-o help-tip-icon" />
</el-tooltip>
</span>
@@ -27,11 +29,7 @@
<template v-if="readonly && hasReadonlyContent">
<div
v-if="data.type === 'input'"
:style="
componentProps.type === 'textarea'
? {padding: '10px 0', lineHeight: 1.5}
: ''
"
:style="componentProps.type === 'textarea' ? { padding: '10px 0', lineHeight: 1.5 } : ''"
>
{{ itemValue }}
</div>
@@ -50,11 +48,7 @@
v-on="listeners"
>
<template v-for="opt in options">
<el-option
v-if="data.type === 'select'"
:key="opt.label"
v-bind="opt"
/>
<el-option v-if="data.type === 'select'" :key="opt.label" v-bind="opt" />
<el-checkbox-button
v-else-if="data.type === 'checkbox-group' && data.style === 'button'"
:key="opt.value"
@@ -111,10 +105,10 @@
<script>
import getEnableWhenStatus from '../util/enable-when'
import { noop } from '../util/utils'
import _get from 'lodash.get'
import _includes from 'lodash.includes'
import _topairs from 'lodash.topairs'
import _frompairs from 'lodash.frompairs'
import _get from 'lodash/get'
import _includes from 'lodash/includes'
import _topairs from 'lodash/toPairs'
import _frompairs from 'lodash/fromPairs'
function validator(data) {
if (!data) {
@@ -152,6 +146,10 @@ export default {
props: {
// eslint-disable-next-line vue/require-default-prop
data: Object,
serverErrors: {
type: Object,
default: () => ({})
},
prop: {
type: String,
default() {
@@ -170,7 +168,8 @@ export default {
data() {
return {
propsInner: {},
isBlurTrigger: this.data.rules &&
isBlurTrigger:
this.data.rules &&
this.data.rules.some(rule => {
return rule.required && rule.trigger === 'blur'
})
@@ -179,7 +178,7 @@ export default {
computed: {
// 解构运算符会处理 undefined 的情况
componentProps: ({ data: { el }, propsInner }) => ({ ...el, ...propsInner }),
hasReadonlyContent: ({ data: { type }}) => _includes(['input', 'select'], type),
hasReadonlyContent: ({ data: { type } }) => _includes(['input', 'select'], type),
hiddenStatus: ({ data: { hidden = () => false }, data, value }) => hidden(value, data),
enableWhenStatus: ({ data: { enableWhen }, value }) => getEnableWhenStatus(enableWhen, value),
// 是否显示
@@ -189,6 +188,11 @@ export default {
classes() {
return 'el-form-item-' + this.data.prop + ' ' + (this.data.attrs?.class || '')
},
errorText() {
const fromAttrs = this.data?.attrs?.error
const fromServer = this.serverErrors ? this.serverErrors[this.data.prop] : ''
return fromAttrs || fromServer || ''
},
listeners() {
const {
data: {
@@ -204,10 +208,7 @@ export default {
} = this
return {
..._frompairs(
_topairs(on).map(([eName, handler]) => [
eName,
(...args) => handler(args, updateForm)
]),
_topairs(on).map(([eName, handler]) => [eName, (...args) => handler(args, updateForm)])
),
// 手动更新表单数据
input: (value, ...rest) => {
@@ -232,9 +233,7 @@ export default {
multipleValue: ({ data, itemValue, options = [] }) => {
const multipleSelectValue =
_get(data, 'el.multiple') && Array.isArray(itemValue)
? itemValue
: [itemValue]
_get(data, 'el.multiple') && Array.isArray(itemValue) ? itemValue : [itemValue]
return multipleSelectValue
.map(val => (options.find(op => op.value === val) || {}).label)
.join()
@@ -254,8 +253,7 @@ export default {
if (v.url === oldV.url || v.request === oldV.request) return
}
const isOptionsCase =
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) >
-1
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) > -1
const {
url,
request = () => this.$axios.get(url).then(resp => resp.data),
@@ -323,7 +321,7 @@ export default {
}
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.help-tips {
opacity: 0.8;
line-height: 2;
@@ -332,7 +330,7 @@ export default {
.help-block {
::v-deep .el-alert__icon {
font-size: 16px
font-size: 16px;
}
&.checkbox {

View File

@@ -1,11 +1,18 @@
<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"
@submit.native.prevent
>
<template v-for="item in innerContent">
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
<component
:is="item.type === GROUP ? 'render-form-group' : 'render-form-item'"
:key="item.id"
:data="item"
:server-errors="serverErrors"
:disabled="disabled || item.disabled"
:item-value="value[item.id]"
:options="options[item.id]"
@@ -19,13 +26,19 @@
</el-form>
</template>
<script>
import _set from 'lodash.set'
import _isequal from 'lodash.isequal'
import _clonedeep from 'lodash.clonedeep'
import _set from 'lodash/set'
import _isequal from 'lodash/isEqual'
import _clonedeep from 'lodash/cloneDeep'
import RenderFormGroup from './components/render-form-group.vue'
import RenderFormItem from './components/render-form-item.vue'
import transformContent from './util/transform-content'
import { collect, correctValue, mergeValue, transformInputValue, transformOutputValue } from './util/utils'
import {
collect,
correctValue,
mergeValue,
transformInputValue,
transformOutputValue
} from './util/utils'
const GROUP = 'group'
@@ -47,6 +60,10 @@ export default {
type: Array,
required: true
},
serverErrors: {
type: Object,
default: () => ({})
},
disabled: {
type: Boolean,
default: false

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

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-sequences */
import _kebabcase from 'lodash.kebabcase'
import _ from 'lodash'
/**
* content 的每一项会浅拷贝一层
* 只可以在 item 层新增修改属性,如 item.a = b
@@ -13,7 +13,7 @@ export default function transformContent(content) {
removeDollarInKey(item)
extractRulesFromComponent(item)
// 有些旧写法是 checkboxGroup & radioGroup
item.type = _kebabcase(item.type)
item.type = _.kebabCase(item.type)
}
return item
@@ -34,8 +34,5 @@ export function extractRulesFromComponent(item) {
if (!component || typeof component === 'string') return
const { rules = [] } = component
item.rules = [
...(item.rules || []),
...(typeof rules === 'function' ? rules(item) : rules)
]
item.rules = [...(item.rules || []), ...(typeof rules === 'function' ? rules(item) : rules)]
}

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,11 +12,9 @@ export function collect(content, key) {
value: item.type === 'group' ? collect(item.items, key) : item[key]
}))
.filter(
({ type, value }) =>
value !== undefined ||
(type === 'group' && Object.keys(value).length),
({ type, value }) => value !== undefined || (type === 'group' && Object.keys(value).length)
)
.map(({ id, value }) => [id, value]),
.map(({ id, value }) => [id, value])
)
}

View File

@@ -11,6 +11,7 @@
:label-width="labelWidth"
:style="{ '--label-width': labelWidth }"
v-bind="$attrs"
:server-errors="serverErrors"
v-on="$listeners"
>
<!-- slot 透传 -->
@@ -55,7 +56,7 @@
<el-button
v-for="button in moreButtons"
v-show="!button.hidden"
v-show="!iHidden(button)"
:key="button.title"
:loading="button.loading"
size="small"
@@ -121,6 +122,10 @@ export default {
type: Boolean,
default: true
},
serverErrors: {
type: Object,
default: () => ({})
},
fields: {
type: Array,
default: () => []
@@ -144,7 +149,7 @@ export default {
},
labelWidth: {
type: String,
default: '25%'
default: '18.2%'
}
},
data() {
@@ -226,6 +231,9 @@ export default {
},
getFormValue() {
return this.$refs.form.getFormValue()
},
iHidden(item) {
return typeof item.hidden === 'function' ? item.hidden() : item.hidden
}
}
}
@@ -236,6 +244,10 @@ export default {
margin-right: 80px;
margin-bottom: 20px;
.el-form {
margin-right: 0;
}
::v-deep .el-input-group__prepend {
border-radius: 0;
}

View File

@@ -1,4 +1,5 @@
import i18n from '@/i18n/i18n'
import request from '@/utils/request'
export const Required = {
required: true, message: i18n.t('FieldRequiredError'), trigger: 'blur'
@@ -118,3 +119,69 @@ export default {
matchAlphanumericUnderscore,
MatchExcludeParenthesis
}
/**
* @description 表单唯一性校验
*
* @param {Object} options
* @param {string} 列表查询地址
* @param {string} 查询参数名
* @param {string} 字段中文名
* @param {string} 字段名
* @param {function(): (string|number)} 返回更新场景下的当前对象 id
*/
export function UniqueCheck(options = {}) {
const { url, param, label, fieldName, getIgnoreId } = options
function existsInResponse(res) {
if (Array.isArray(res)) return res.length > 0
if (res && typeof res === 'object') {
if (typeof res.count === 'number') return res.count > 0
if (Array.isArray(res.results)) return res.results.length > 0
}
return !!res
}
function extractIds(res) {
if (Array.isArray(res)) return res.map(i => i?.id).filter(Boolean)
if (res && Array.isArray(res.results)) return res.results.map(i => i?.id).filter(Boolean)
return []
}
return {
async validator(rule, value, callback) {
try {
let v = value
if (typeof v === 'string') v = v.trim()
if (v === '' || v === undefined || v === null) return callback()
if (!url || !param) return callback()
const res = await request.get(url, { params: { [param]: v } })
let duplicated = existsInResponse(res)
if (duplicated && typeof getIgnoreId === 'function') {
const curId = getIgnoreId()
if (curId) {
const ids = extractIds(res)
// 查询结果只包含自身,因此不被视为重复
if (ids.length >= 1 && ids.every(id => id === curId)) {
duplicated = false
}
}
}
if (duplicated) {
const _label = label || fieldName || ''
const msg = `${_label}${i18n.t('Existing')}`
callback(new Error(msg))
} else {
callback()
}
} catch (e) {
callback()
}
},
trigger: ['blur']
}
}

View File

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

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({}, this.$attrs)
const attrs = Object.assign({ clearable: this.clearable }, this.$attrs)
delete attrs.value
return attrs
},
@@ -50,6 +50,13 @@ export default {
const value = this.objectsToValues(this.value)
return value
}
},
clearable() {
if (this.$attrs.clearable === undefined) {
return this.multiple
} else {
return this.$attrs.clearable
}
}
},
methods: {
@@ -71,8 +78,11 @@ export default {
value = value.map(v => {
// uuid v4
const uuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
return typeof v === 'object' ? v
: this.$attrs?.allowCreate && !uuid.test(v) ? { [this.customLabelKeyName]: v } : { pk: v }
return typeof v === 'object'
? v
: this.$attrs?.allowCreate && !uuid.test(v)
? { [this.customLabelKeyName]: v }
: { pk: v }
})
if (!this.multiple) {
value = value[0]
@@ -87,9 +97,13 @@ export default {
if (!Array.isArray(val)) {
val = [val]
}
val = val.map((v) => {
val = val.map(v => {
if (v && typeof v === 'object') {
return v.pk || v.id || (this.$attrs?.allowCreate ? (v?.[this.customLabelKeyName] + ':' + v?.value) : '')
return (
v.pk ||
v.id ||
(this.$attrs?.allowCreate ? v?.[this.customLabelKeyName] + ':' + v?.value : '')
)
} else {
return v
}
@@ -103,6 +117,4 @@ export default {
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -33,6 +33,9 @@
>
<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>
@@ -140,6 +143,11 @@ export default {
},
handleShowPassword() {
this.isCheckShowPassword = !this.isCheckShowPassword
},
handleClearAll() {
this.filterTags = []
this.$emit('change', this.filterTags)
this.$emit('input', this.filterTags)
}
}
}
@@ -161,11 +169,12 @@ export default {
line-height: 30px;
&:hover {
border-color: #C0C4CC;
border-color: #c0c4cc;
}
& ::v-deep .el-tag {
margin-bottom: 1px;
margin-bottom: 2px;
margin-top: 2px;
font-family: sans-serif !important;
margin-left: 5px;
}
@@ -177,6 +186,7 @@ export default {
.search-input {
flex: 1;
min-width: 150px;
& ::v-deep .el-input__inner {
max-width: 100%;
@@ -204,4 +214,15 @@ 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

@@ -5,6 +5,11 @@
<slot name="no-data" />
</template>
<template v-else>
<!--
过滤 selection 相关事件的透传避免父组件收到 el-table 原生的当前页 selection
导致跨页选择persistSelection被覆盖只剩当页数据
选择事件统一走 selectStrategy在内部维护全量 selected 并向外 emit
-->
<el-table
ref="table"
v-loading="tableLoading"
@@ -12,7 +17,7 @@
:row-class-name="rowClassName"
v-bind="tableAttrs"
@select="selectStrategy.onSelect"
v-on="$listeners"
v-on="forwardListeners"
@selection-change="selectStrategy.onSelectionChange"
@select-all="handleSelectAll($event, canSelect)"
@sort-change="onSortChange"
@@ -23,27 +28,18 @@
<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>
@@ -52,23 +48,16 @@
<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)"
@@ -84,14 +73,19 @@
<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"
@@ -100,14 +94,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"
@@ -161,9 +155,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'
@@ -275,8 +269,7 @@ export default {
*/
beforeSearch: {
type: Function,
default() {
}
default() {}
},
/**
* 单选, 适用场景: 不可以批量删除
@@ -441,8 +434,7 @@ export default {
*/
onEdit: {
type: Function,
default(row) {
}
default(row) {}
},
/**
* 点击删除按钮时的方法, 当默认删除方法不满足需求时使用, 需要返回promise
@@ -451,9 +443,7 @@ 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)
}
},
@@ -707,8 +697,8 @@ export default {
}
},
/*
* 设置默认对齐方式
*/
* 设置默认对齐方式
*/
defaultAlign: {
type: String,
default: 'center'
@@ -723,8 +713,7 @@ export default {
},
extraPaginationAttrs: {
type: Object,
default: () => {
}
default: () => {}
},
hasSelection: {
type: Boolean,
@@ -810,6 +799,16 @@ export default {
selectStrategy() {
return getSelectStrategy(this)
},
// 过滤会与内部选择策略冲突的事件,避免父组件只拿到当前页 selection
forwardListeners() {
const listeners = { ...this.$listeners }
delete listeners['selection-change']
delete listeners['select']
delete listeners['select-all']
// 外层如需监听 selection 变化,请监听本组件透出的 selection-change
// 该事件来自选择策略,已汇总跨页后的全量 selected
return listeners
},
searchLocatedSlotKeys() {
return getLocatedSlotKeys(this.$slots, 'search:')
},
@@ -897,9 +896,7 @@ 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
@@ -981,9 +978,7 @@ 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
@@ -1003,10 +998,7 @@ 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) || []
@@ -1189,7 +1181,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
@@ -1225,11 +1217,7 @@ 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--
}
},
@@ -1257,20 +1245,14 @@ 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,14 +1,9 @@
/**
* 两种多选策略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)
@@ -18,39 +13,22 @@ class StrategyAbstract {
return this.elDataTable.$refs.table
}
onSelectionChange() {
}
onSelect() {
}
onSelectAll() {
}
toggleRowSelection() {
}
clearSelection() {
}
updateElTableSelection() {
}
onSelectionChange() {}
onSelect() {}
onSelectAll() {}
toggleRowSelection() {}
clearSelection() {}
updateElTableSelection() {}
}
/**
* 普通策略。由el-table维护selected
* 普通策略。由 el-table 自己维护 selection
*/
class StrategyNormal extends StrategyAbstract {
/**
* normal模式下只需要监听selection-change事件
*/
onSelectionChange(val) {
this.elDataTable.selected = val
}
/**
* toggleRowSelection和clearSelection的表现与el-table一致
*/
toggleRowSelection(...args) {
return this.elTable.toggleRowSelection(...args)
}
@@ -61,44 +39,28 @@ 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()
@@ -106,15 +68,11 @@ 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)
@@ -122,17 +80,12 @@ 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])
@@ -149,26 +102,24 @@ 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

@@ -107,8 +107,8 @@ export default {
const formatterArgs = value?.formatterArgs
// console.log('>>> name: ', key)
// console.log('>>> formatter: ', formatter)
const detailFormaters = ['AmountFormatter', 'DetailFormatter']
if (formatter && detailFormaters.includes(formatter.name) && formatterArgs.drawer !== false) {
const detailFormatters = ['AmountFormatter', 'DetailFormatter']
if (formatter && detailFormatters.includes(formatter.name) && formatterArgs.drawer !== false) {
formatterArgs.onClick = this.onDetail
}
}
@@ -223,7 +223,6 @@ export default {
}
title = actionLabel + this.$t('WordSep') + toLowerCaseExcludeAbbr(title)
return title
},
getDefaultDrawer(action) {
@@ -295,7 +294,6 @@ 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) {
@@ -312,6 +310,7 @@ 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()

View File

@@ -58,6 +58,7 @@
:import-option="importOption"
:json-data="jsonData"
:url="url"
v-bind="$attrs"
@cancel="cancelUpload"
@finish="closeDialog"
/>
@@ -247,46 +248,46 @@ export default {
</script>
<style lang='scss' scoped>
@import "~@/styles/variables";
@import "~@/styles/variables";
.error-msg {
color: $--color-danger;
.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.error-results {
background-color: #f3f3f4;
max-height: 200px;
overflow: auto
.tableFilter {
padding-bottom: 10px;
}
}
.file-uploader ::v-deep .el-upload {
width: 100%;
//padding-right: 150px;
}
.importTable ::v-deep .el-dialog__body {
padding-bottom: 20px;
}
.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 {
margin-left: 80px;
}
.export-item:first-child {
margin-left: 0;

View File

@@ -97,6 +97,10 @@ export default {
origin: {
type: String,
default: ''
},
encryptFields: {
type: Array,
default: () => []
}
},
data() {
@@ -273,11 +277,15 @@ 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 = ['password', 'secret', 'private_key']
const encryptFields = this.getEncryptFields()
for (const field of encryptFields) {
if (item[field]) {
item[field] = encryptPassword(item[field])

View File

@@ -86,13 +86,11 @@ export default {
},
datePick: {
type: Function,
default: (val) => {
}
default: val => {}
},
searchTable: {
type: Function,
default: (val) => {
}
default: val => {}
},
selectedRows: {
type: Array,
@@ -153,7 +151,7 @@ export default {
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
$innerHeight: 28px;
$headerHeight: 30px;
@@ -239,5 +237,4 @@ $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"
@@ -85,10 +85,13 @@ 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
@@ -169,7 +172,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')
}
@@ -390,6 +393,6 @@ export default {
//修改颜色
.el-button--text {
color: #409EFF;
color: #409eff;
}
</style>

View File

@@ -23,7 +23,7 @@ const defaultPerformDelete = function({ row, col }) {
const defaultUpdateCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}
let route = { params: { id: id } }
const updateRoute = this.colActions.updateRoute
if (typeof updateRoute === 'object') {
@@ -39,7 +39,7 @@ const defaultUpdateCallback = function({ row, col }) {
const defaultViewCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}
let route = { params: { id: id } }
const viewRoute = this.colActions.viewRoute
if (typeof updateRoute === 'object') {
@@ -55,7 +55,7 @@ const defaultViewCallback = function({ row, col }) {
const defaultCloneCallback = function({ row, col }) {
const id = row.id
let route = { query: { clone_from: id }}
let route = { query: { clone_from: id } }
const cloneRoute = this.colActions.cloneRoute
if (typeof cloneRoute === 'object') {
@@ -82,7 +82,7 @@ const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
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

@@ -16,7 +16,7 @@
closable
size="small"
type="info"
@click="handleTagClick(v,k)"
@click="handleTagClick(v, k)"
@close.stop="handleTagClose(k)"
>
<strong v-if="v.label">{{ v.label + ':' }}</strong>
@@ -40,7 +40,6 @@
/>
<span :class="isFocus ? 'is-focus ' : ''" class="keydown-focus">/</span>
</div>
</template>
<script>
@@ -49,8 +48,7 @@ export default {
props: {
config: {
type: Object,
default: () => {
}
default: () => {}
},
options: {
type: Array,
@@ -90,13 +88,14 @@ export default {
},
filterMaps() {
const data = {}
const keyword = 'search'
for (let key in this.filterTags) {
const value = this.filterTags[key]['value']
if (key === '') {
key = 'search'
key = keyword
}
if (key.startsWith('search')) {
data['search'] = (data.search ? data.search + ',' : '') + value
if (key.startsWith(keyword)) {
data[keyword] = (data[keyword] ? data[keyword] + ',' : '') + value
} else {
data[key] = value
}
@@ -138,7 +137,7 @@ export default {
this.emptyCount = 1
}
},
'$route'(to, from) {
$route(to, from) {
if (from.query !== to.query) {
this.filterTags = {}
if (to.query && Object.keys(to.query).length) {
@@ -169,7 +168,7 @@ export default {
// 获取url中的查询条件判断是不是包含在当前查询条件里
checkInTableColumns(options) {
const searchFieldOptions = {}
const queryInfoValues = options.map((i) => i.value)
const queryInfoValues = options.map(i => i.value)
const routeQuery = this.getUrlQuery ? this.$route?.query : {}
const routeQueryKeysLength = Object.keys(routeQuery).length
if (routeQueryKeysLength < 1) return searchFieldOptions

View File

@@ -8,7 +8,7 @@
class="left"
>
<span v-if="component === 'AutoDataZTree'" class="title">
{{ $t('AssetTree') }}
{{ title }}
</span>
<component
:is="component"
@@ -111,6 +111,13 @@ export default {
headerActions: {
type: Object,
default: () => ({})
},
title: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: String,
default() {
return this.$t('AssetTree')
}
}
},
data() {

View File

@@ -421,12 +421,8 @@ export default {
display: inline-block;
}
.data-z-tree {
::v-deep {
.icon {
width: 10px;
margin-right: 3px;
}
}
.data-z-tree ::v-deep .icon {
width: 10px;
margin-right: 3px;
}
</style>

View File

@@ -1,16 +1,67 @@
<template>
<div>
<div class="treebox">
<div v-if="treeSetting.showSearch">
<div v-if="treeSetting.showSearch" @click="focusTreeSearchInput">
<el-input
v-show="showTreeSearch"
ref="treeSearchInput"
v-model="treeSearchValue"
:placeholder="$tc('Search')"
class="fixed-tree-search"
prefix-icon="fa fa-search"
:placeholder="treeSearchInputPlaceholder"
size="mini"
@input="treeSearchHandle"
>
<template #prepend>
<template v-if="!isSearchTypeDropdownEnabled">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="300"
>
<span style="cursor: pointer;" @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
</span>
</el-tooltip>
</template>
<template v-else>
<el-dropdown trigger="hover" @command="onSearchTypeChange">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="1000"
>
<span @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
<i class="el-icon-arrow-down" />
</span>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="(item, type) in treeSearchTypeOptions"
:key="type"
:command="type"
:class="{ 'is-active': treeSearchType === type }"
>
<el-tooltip
effect="dark"
placement="right"
:content="item.tooltip"
:open-delay="300"
>
<span>{{ item.label }}</span>
</el-tooltip>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</template>
<span slot="suffix">
<i
class="el-icon-close"
@@ -47,6 +98,7 @@ import '@ztree/ztree_v3/js/jquery.ztree.exhide.min.js'
import '@/styles/ztree.css'
import '@/styles/ztree_icon.scss'
import axiosRetry from 'axios-retry'
import { setUrlParam } from '@/utils/common'
const defaultObject = {
type: Object,
@@ -66,18 +118,52 @@ export default {
rMenu: '',
init: false,
loading: false,
showTreeSearch: false,
treeSearchValue: ''
showTreeSearch: true,
treeSearchValue: '',
treeSearchType: 'asset',
treeSearchTypeOptions: {},
treeSearchTypeSupportOptions: {
node: {
label: this.$t('Node'),
placeholder: this.$t('Search node'),
tooltip: this.$t('Search by node name'),
search_key: 'search_node'
},
asset: {
label: this.$t('Asset'),
placeholder: this.$t('Search asset'),
tooltip: this.$t('Search by asset name or address'),
search_key: 'search_asset'
}
},
treeType: '' // asset | node
}
},
computed: {
treeSetting() {
return this.setting
},
isSearchTypeDropdownEnabled() {
return Object.keys(this.treeSearchTypeOptions).length > 1
},
currentTreeSearchType() {
return this.treeSearchTypeOptions[this.treeSearchType]
},
currentTreeSearchTypeTooltip() {
return this.currentTreeSearchType?.tooltip || ''
},
treeSearchTypeLabel() {
return this.currentTreeSearchType?.label || ''
},
treeSearchInputPlaceholder() {
return this.currentTreeSearchType?.placeholder || this.$t('Search')
}
},
mounted() {
window.refresh = this.refresh
window.onSearch = this.onSearch
this.initTreeType()
this.initTreeSearchTypeOptions()
this.initTree().then(() => {
this.$nextTick(() => {
this.updateTreeHeight()
@@ -90,6 +176,36 @@ export default {
window.removeEventListener('resize', this.updateTreeHeight)
},
methods: {
initTreeType() {
let treeType = this.treeSetting.treeType
if (!treeType) {
treeType = this.treeSetting.async?.enable ? 'asset' : 'node'
}
this.treeType = treeType
},
initTreeSearchTypeOptions() {
if (this.treeType === 'asset') {
// 资产树支持异步搜索节点和资产
this.treeSearchTypeOptions = this.treeSearchTypeSupportOptions
// 默认搜索资产
this.treeSearchType = 'asset'
} else {
// 节点树只支持搜索节点
this.treeSearchTypeOptions = Object.fromEntries(
Object.entries(this.treeSearchTypeSupportOptions)
.filter(([key]) => key === 'node')
)
// 默认搜索节点
this.treeSearchType = 'node'
}
},
onSearchTypeChange(type) {
this.treeSearchType = type
this.focusTreeSearchInput()
},
focusTreeSearchInput() {
this.$refs.treeSearchInput.focus()
},
onMenuClick(menu) {
if (menu.disabled) {
return
@@ -116,19 +232,14 @@ export default {
const zTreeRect = tree.getBoundingClientRect()
tree.style.height = `calc(100vh - ${zTreeRect.top}px - 30px - 25px)`
}, 100),
async initTree(refresh = false) {
async initTree(refresh = false, iTreeUrl = '') {
const vm = this
let treeUrl
this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 &&
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
: `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
let treeUrl = iTreeUrl
if (!treeUrl) {
treeUrl = this.treeSetting.treeUrl
}
treeUrl = setUrlParam(treeUrl, 'tree_type', this.treeType)
if (refresh) {
$.fn.zTree.destroy(this.iZTreeID)
@@ -221,25 +332,31 @@ export default {
searchInput.oninput = e => this.treeSearchHandle((e.target.value || ''))
},
treeSearchHandle: _.debounce(function(value) {
if (this.treeSetting.async.enable) {
this.filterAssetsServer(value)
if (this.treeSetting.async?.enable) {
this.searchFromServer(value)
} else {
this.filterTree(value)
this.searchFromLocal(value)
}
}, 600),
getCheckedNodes: function() {
return this.zTree.getCheckedNodes(true)
},
recurseParent(node) {
const parentNode = node.getParentNode()
if (parentNode && parentNode.pId) {
return [parentNode, ...this.recurseParent(parentNode)]
} else if (parentNode) {
return [parentNode]
} else {
if (!parentNode) {
return []
}
const allParents = []
if (parentNode) {
allParents.push(parentNode)
if (parentNode.pId) {
allParents.push(...this.recurseParent(parentNode))
}
}
return allParents
},
recurseChildren(node) {
if (!node.isParent) {
return []
@@ -248,49 +365,27 @@ export default {
if (!children) {
return []
}
let allChildren = []
const allChildren = []
children.forEach((n) => {
allChildren = [...children, ...this.recurseChildren(n)]
allChildren.push(n)
allChildren.push(...this.recurseChildren(n))
})
return allChildren
},
groupBy(array, filter) {
const groups = {}
array.forEach(function(o) {
const group = JSON.stringify(filter(o))
groups[group] = groups[group] || []
groups[group].push(o)
})
return Object.keys(groups).map(function(group) {
return groups[group]
})
},
filterTree(keyword, tree = this.zTree) {
searchFromLocal(keyword, tree = this.zTree) {
if (!this.zTree) return
const searchNode = tree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) tree.removeNode(searchNode[0])
const nodes = tree.transformToArray(tree.getNodes())
const allNodes = tree.transformToArray(tree.getNodes())
if (!keyword) {
tree.showNodes(nodes)
tree.showNodes(allNodes)
tree.expandAll(false)
return
}
if (!keyword) {
if (tree.hiddenNodes) {
tree.showNodes(tree.hiddenNodes)
tree.hiddenNodes = null
}
if (tree.expandNodes) {
tree.expandNodes.forEach((node) => {
if (node.id !== nodes[0].id) {
tree.expandNode(node, false)
}
})
tree.expandNodes = null
}
return null
}
let shouldShow = []
const matchedNodes = tree.getNodesByFilter((node) => {
return node.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1
})
@@ -300,67 +395,54 @@ export default {
const assetsAmount = matchedNodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: false, open: false }
tree.addNodes(null, newNode)
const addedNodes = tree.addNodes(null, newNode)
// 隐藏所有节点,只显示搜索节点
tree.hideNodes(allNodes)
tree.showNodes(addedNodes)
return
}
// 获取应该展示的节点,以及应该展开的节点
let shouldShow = []
let shouldExpandNodes = []
let shouldCollapseNodes = []
matchedNodes.forEach((node) => {
const parents = this.recurseParent(node)
const children = this.recurseChildren(node)
shouldShow = [...shouldShow, ...parents, ...children, node]
})
// 应该显示匹配节点本身、其祖先节点和子孙节点
shouldShow.push(node)
shouldShow.push(...parents)
shouldShow.push(...children)
tree.hiddenNodes = nodes
tree.expandNodes = shouldShow
tree.hideNodes(nodes)
// 应该展开匹配节点的父节点,不展开匹配节点的子孙节点
shouldExpandNodes.push(...parents)
// 应该折叠匹配节点的子孙节点
shouldCollapseNodes.push(node)
shouldCollapseNodes.push(...children)
})
shouldShow = Array.from(new Set(shouldShow))
shouldExpandNodes = Array.from(new Set(shouldExpandNodes))
shouldCollapseNodes = Array.from(new Set(shouldCollapseNodes))
// 隐藏所有节点,显示应该显示的节点
tree.hideNodes(allNodes)
tree.showNodes(shouldShow)
for (const node of shouldShow) {
if (node.isParent) {
tree.expandNode(node, true)
}
// 展开应该展开的节点
for (const node of shouldExpandNodes) {
tree.expandNode(node, true)
}
// 折叠应该折叠的节点
for (const node of shouldCollapseNodes) {
tree.expandNode(node, false)
}
},
filterAssetsServer(keyword) {
if (!this.zTree) return
let searchNode = this.zTree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) {
this.zTree.removeChildNodes(searchNode[0])
this.zTree.removeNode(searchNode[0])
}
const treeNodes = this.zTree.getNodes()
if (!keyword) {
if (treeNodes.length !== 0) {
this.zTree.showNodes(treeNodes)
}
return
}
if (treeNodes.length !== 0) {
this.zTree.hideNodes(treeNodes)
}
let treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const filterField = treeUrl.includes('?') ? `&search=${keyword}` : `?search=${keyword}`
if (treeUrl.indexOf('assets/nodes/children/tree') > -1) {
treeUrl = treeUrl + '&all=all'
}
const searchUrl = `${treeUrl}${filterField}`
this.$axios.get(searchUrl).then(nodes => {
let name = this.$t('Search')
const assetsAmount = nodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: true, open: true, zAsync: true }
searchNode = this.zTree.addNodes(null, newNode)[0]
searchNode.zAsync = true
this.rootNodeAddDom(searchNode)
const nodesGroupByOrg = this.groupBy(nodes, (node) => {
return node.meta?.data?.org_name
})
for (const item of nodesGroupByOrg) {
this.zTree.addNodes(searchNode, item)
}
searchNode.open = true
})
searchFromServer(keyword) {
// 直接用搜索 API 返回的数据重新初始化树
const treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const searchTypeKey = this.treeSearchTypeOptions[this.treeSearchType]?.search_key || 'search'
const searchUrl = setUrlParam(treeUrl, searchTypeKey, keyword)
this.initTree(true, searchUrl)
}
}
@@ -624,12 +706,19 @@ div.rMenu li {
.fixed-tree-search {
margin-bottom: 10px;
border: 1px solid;
border-radius: 3px;
&:hover,
&:focus-within {
border-color: var(--color-primary);
}
& ::v-deep .el-input__inner {
border-radius: 4px;
border: none;
background: #fafafa;
padding-right: 32px;
color: var(--color-text-primary)
color: var(--color-text-primary);
}
& ::v-deep .el-input__suffix {
@@ -653,6 +742,37 @@ div.rMenu li {
& ::v-deep .el-input__suffix-inner {
line-height: 30px;
}
& ::v-deep .el-input-group__prepend {
padding-left: 5px;
padding-right: 3px;
border: none;
color: #999;
* {
color: inherit;
}
align-items: center;
background: #fafafa;
.el-icon-arrow-down {
display: inline-block;
transition: transform 0.8s ease; /* 动画关键 */
}
:hover {
.el-icon-arrow-down {
transform: rotate(180deg); /* 顺时针 180° */
}
}
.search-label {
margin-left: 1px;
margin-right: 1px;
}
}
}
::v-deep .el-dropdown-menu__item.is-active {
color: var(--color-primary);
font-weight: 500;
}
.icon-refresh {

View File

@@ -1,20 +1,52 @@
<template>
<el-alert
v-if="enabled && !isViewed()"
:center="false"
:title="title"
class="announcement"
type="success"
@close="onClose"
>
<MarkDown :value="announcement.content" class="markdown" />
<span v-if="announcement.link">
<el-link :href="announcement.link" class="link-more" target="_blank" type="info">
{{ $t('ViewMore') }}
</el-link>
<i class="fa fa-external-link icon" />
</span>
</el-alert>
<div>
<el-dialog
v-if="enabled && showModal"
:visible.sync="dialogVisible"
:title="title"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
width="35%"
class="announcement-dialog"
center
>
<div class="announcement-content">
<div class="content-wrapper">
<MarkDown :value="announcement.content" class="markdown" />
</div>
<div v-if="announcement.link" class="link-section">
<el-link :href="announcement.link" class="link-more" target="_blank" type="info">
{{ $t('ViewMore') }}
</el-link>
<i class="fa fa-external-link icon" />
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="onModalConfirm">
{{ $t('Confirm') }}
</el-button>
</div>
</el-dialog>
<el-alert
v-if="enabled && showAlert"
:center="false"
:title="title"
class="announcement"
type="success"
@close="onAlertClose"
>
<MarkDown :value="announcement.content" class="markdown" />
<span v-if="announcement.link">
<el-link :href="announcement.link" class="link-more" target="_blank" type="info">
{{ $t('ViewMore') }}
</el-link>
<i class="fa fa-external-link icon" />
</span>
</el-alert>
</div>
</template>
<script>
@@ -26,7 +58,11 @@ export default {
components: { MarkDown },
data() {
return {
viewedKey: 'AnnouncementViewed'
viewedKey: 'AnnouncementViewed',
modalConfirmedKey: 'AnnouncementModalConfirmed',
dialogVisible: false,
modalConfirmed: false,
alertViewed: false
}
},
computed: {
@@ -58,48 +94,200 @@ export default {
const start = new Date(this.announcement.date_start)
const end = new Date(this.announcement.date_end)
return now >= start && now <= end
},
showModal() {
return !this.modalConfirmed
},
showAlert() {
return this.modalConfirmed && !this.alertViewed
}
},
watch: {
enabled: {
handler(val) {
if (val) {
this.initializeState()
this.checkAndShowDialog()
}
},
immediate: true
}
},
mounted() {
this.initializeState()
this.checkAndShowDialog()
},
methods: {
onClose() {
localStorage.setItem(this.viewedKey, this.announcement.id)
initializeState() {
this.modalConfirmed = this.isModalConfirmed()
this.alertViewed = this.isAlertViewed()
},
isViewed() {
checkAndShowDialog() {
if (this.enabled && this.showModal) {
this.dialogVisible = true
}
},
onModalConfirm() {
localStorage.setItem(this.modalConfirmedKey, this.announcement.id)
this.modalConfirmed = true
this.dialogVisible = false
this.$emit('announcement-modal-confirmed', {
id: this.announcement.id,
subject: this.announcement.subject,
confirmedAt: new Date().toISOString()
})
},
onAlertClose() {
localStorage.setItem(this.viewedKey, this.announcement.id)
this.alertViewed = true
this.$emit('announcement-read', {
id: this.announcement.id,
subject: this.announcement.subject,
readAt: new Date().toISOString()
})
},
isModalConfirmed() {
const confirmedId = localStorage.getItem(this.modalConfirmedKey)
return confirmedId === this.announcement.id
},
isAlertViewed() {
const viewedId = localStorage.getItem(this.viewedKey)
return viewedId === this.announcement.id
},
isViewed() {
return this.isAlertViewed()
}
}
}
</script>
<style lang="scss" scoped>
.announcement ::v-deep .el-alert__content {
width: 100%;
.announcement-dialog ::v-deep .el-dialog {
border-radius: 5px;
}
.announcement-main {
word-wrap: break-word;
white-space: pre-wrap;
.announcement-dialog ::v-deep .el-dialog__wrapper {
backdrop-filter: blur(4px);
}
@media (max-width: 768px) {
.announcement-dialog ::v-deep .el-dialog {
width: 95% !important;
margin: 0 auto;
}
.announcement-content {
padding: 20px 25px 15px;
.content-wrapper {
padding: 15px;
}
}
.dialog-footer {
.el-button {
padding: 10px;
font-size: 14px;
}
}
}
.announcement-dialog ::v-deep .el-dialog__header {
border-radius: 8px 8px 0 0;
padding: 10px 20px 10px 20px;
.el-dialog__title {
color: black;
font-weight: 600;
}
}
.announcement-dialog ::v-deep .el-dialog__body {
padding: 0;
}
.announcement-content {
padding: 25px 30px 20px;
.link-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #e4e7ed;
text-align: center;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
align-items: center;
.el-button {
padding: 10px 20px;
font-size: 14px;
}
}
.announcement ::v-deep .el-alert__content {
width: 100%;
}
.icon {
vertical-align: text-bottom;
color: var(--color-info) !important;
margin-left: 5px;
}
.markdown {
background-color: transparent !important;
font-size: 14px;
line-height: 1.8;
color: #2c3e50;
.link-more {
font-size: 10px;
margin-left: 10px;
border-bottom: solid 1px;
color: var(--color-info) !important;
font-size: 13px;
color: #409eff !important;
text-decoration: none;
padding: 4px 8px;
border-radius: 4px;
background: rgba(64, 158, 255, 0.1);
transition: all 0.3s;
&:hover {
background: #409eff;
color: white !important;
}
}
h1, h2, h3, h4, h5 {
margin-top: 0;
margin-bottom: 10px;
h1, h2, h3, h4, h5, h6 {
margin: 0 0 15px 0;
color: #303133;
font-weight: 600;
line-height: 1.4;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
h4, h5, h6 {
font-size: 14px;
}
p {
margin-bottom: 16px;
line-height: 1.8;
color: #4b5563;
}
}
</style>

View File

@@ -33,7 +33,7 @@ export default {
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: 'dashboard' }}].concat(matched)
matched = [{ path: '/dashboard', meta: { title: 'dashboard' } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)

View File

@@ -1,6 +1,6 @@
<script>
export default {
name: 'Index',
name: 'Icon',
props: {
icon: {
type: String,

View File

@@ -37,3 +37,4 @@ export { default as Pagination } from './Table/Pagination'
export { default as Tooltip } from './Widgets/Tooltip'
export { default as ResourceActivity } from './Apps/ResourceActivity'
export { default as MarkDown } from './Widgets/MarkDown'
export { default as VariablesHelpTextDialog } from './Apps/VariablesHelpTextDialog'

View File

@@ -1,6 +1,6 @@
// i18n.js
import Vue from 'vue'
import locale from 'elementui-lts/lib/locale'
import locale from 'element-ui/lib/locale'
import VueI18n from 'vue-i18n'
import messages from './langs'
import date from './date'
@@ -54,7 +54,7 @@ export async function fetchTranslationsFromAPI() {
}
}
} catch (error) {
alert(error)
console.log(error)
} finally {
await store.dispatch('app/setI18nLoaded', true)
}

View File

@@ -1,45 +1,44 @@
import zhLocale from 'elementui-lts/lib/locale/lang/zh-CN'
import zhTWLocale from 'elementui-lts/lib/locale/lang/zh-TW'
import enLocale from 'elementui-lts/lib/locale/lang/en'
import jaLocale from 'elementui-lts/lib/locale/lang/ja'
import ptBrLocale from 'elementui-lts/lib/locale/lang/pt-br'
import esLocale from 'elementui-lts/lib/locale/lang/es'
import ruLocale from 'elementui-lts/lib/locale/lang/ru-RU'
import koLocale from 'elementui-lts/lib/locale/lang/ko'
import zh from './zh.json'
import zhHant from './zh_hant.json'
import zh_hant from './zh_hant.json'
import en from './en.json'
import ja from './ja.json'
const messages = {
zh: {
...zhLocale,
...zh
},
zh_hant: {
...zhTWLocale,
...zhHant
},
en: {
...enLocale,
...en
},
ja: {
...jaLocale,
...ja
},
pt_br: {
...ptBrLocale
},
es: {
...esLocale
},
ru: {
...ruLocale
},
ko: {
...koLocale
// Map app locales to Element-UI locale file names
const elementLocaleNameByAppLocale = {
zh: 'zh-CN',
zh_hant: 'zh-TW',
en: 'en',
ja: 'ja',
pt_br: 'pt-br',
es: 'es',
ru: 'ru-RU',
ko: 'ko',
vi: 'vi'
}
function loadElementLocale(localeName) {
try {
// eslint-disable-next-line import/no-dynamic-require, global-require
const mod = require(`element-ui/lib/locale/lang/${localeName}`)
return (mod && (mod.default || mod)) || {}
} catch (e) {
return {}
}
}
const appLocaleMessages = {
zh,
zh_hant,
en,
ja
}
const messages = Object.keys(elementLocaleNameByAppLocale).reduce((acc, appLocale) => {
const elementLocaleName = elementLocaleNameByAppLocale[appLocale]
const elementLocale = loadElementLocale(elementLocaleName)
const appMessages = appLocaleMessages[appLocale] || {}
acc[appLocale] = { ...elementLocale, ...appMessages }
return acc
}, {})
export default messages

View File

@@ -78,6 +78,3 @@ export default {
}
}
</style>
<style lang="scss">
</style>

View File

@@ -225,24 +225,9 @@ export default {
default(error, method, vm) {
const response = error.response
const data = response.data
if (response.status === 400) {
for (const key of Object.keys(data)) {
let err = ''
let errorTips = data[key]
if (errorTips instanceof Array) {
errorTips = _.filter(errorTips, (item) => Object.keys(item).length > 0)
for (const i of errorTips) {
if (i instanceof Object) {
err += i?.port?.join(',')
} else {
err += i
}
}
} else {
err = errorTips
}
this.$refs.form.setFieldError(key, err)
}
if (response.status === 400 && data && typeof data === 'object') {
// 覆盖式设置错误映射,避免触发表单内容重建
this.$refs.form.setErrors(data)
}
this.$emit('performError', data)
}

View File

@@ -137,14 +137,15 @@ export default {
this.$emit('submitError', error)
const response = error.response
const data = response.data
if (response.status === 400) {
for (const key of Object.keys(data)) {
let value = data[key]
if (value instanceof Array) {
value = value.join(';')
}
this.$refs.form.setFieldError(key, value)
}
// 不要逐个设置字段的 attrs.error 或改动 fields 引用。
// 这样会触发表单 content 重建,导致用户已输入的内容被覆盖/清空,
// 且可能出现只能显示一个字段错误的现象。
// 这里改为使用 AutoDataForm 暴露的 setErrors(errors) 覆盖式设置:
// - 直接同步到 UI 的 el-form-item.validateMessage
// - 支持同时显示多个字段错误
// - 不修改 fields/attrs 引用,避免输入丢失
if (response.status === 400 && data && typeof data === 'object') {
this.$refs.form.setErrors(data)
}
})
}

View File

@@ -71,7 +71,7 @@ export default {
},
async logout() {
const currentOrg = this.$store.getters.currentOrg
if (currentOrg.autoEnter || currentOrg.is_system) {
if (currentOrg && (currentOrg.autoEnter || currentOrg.is_system)) {
await this.$store.dispatch('users/setCurrentOrg', this.$store.getters.preOrg)
}
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`

View File

@@ -4,6 +4,7 @@
:placeholder="$tc('Select')"
:value="currentOrgId"
class="org-select"
:style="{ width: selectWidth }"
filterable
popper-class="switch-org"
@change="changeOrg"
@@ -54,7 +55,8 @@ export default {
},
data() {
return {
orgOption: []
orgOption: [],
selectWidth: 'auto'
}
},
computed: {
@@ -63,6 +65,17 @@ export default {
'usingOrgs',
'currentViewRoute'
]),
currentOrgDisplayName() {
const currentOrgId = this.currentOrg?.id
if (!currentOrgId) {
return this.$tc('Select')
}
const matchedOrg = this.usingOrgs.find(item => item.id === currentOrgId)
if (matchedOrg?.name) {
return matchedOrg.name
}
return this.currentOrg.name || this.$tc('Select')
},
orgActionsGroup() {
const orgActions = {
label: this.$t('OrganizationList'),
@@ -107,7 +120,53 @@ export default {
return currentOrgId
}
},
watch: {
currentOrgDisplayName() {
this.updateWidth()
}
},
mounted() {
this.updateWidth()
},
methods: {
updateWidth() {
this.$nextTick(() => {
// 创建临时元素来测量文本宽度
const tempSpan = document.createElement('span')
tempSpan.style.visibility = 'hidden'
tempSpan.style.position = 'absolute'
tempSpan.style.whiteSpace = 'nowrap'
tempSpan.style.fontSize = '14px'
tempSpan.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
tempSpan.style.fontWeight = 'normal'
tempSpan.style.letterSpacing = 'normal'
// 获取当前组织显示名称
const orgName = this.currentOrgDisplayName
tempSpan.textContent = orgName
document.body.appendChild(tempSpan)
// 测量文本宽度
const textWidth = tempSpan.offsetWidth
// 固定空间:左侧图标 + padding + 右侧箭头
const iconWidth = 15 // 左侧图标
const paddingWidth = 35 // 左右 padding
const arrowWidth = 20 // 右侧箭头
const totalWidth = textWidth + iconWidth + paddingWidth + arrowWidth
// 设置合理的边界
const minWidth = 100
const maxWidth = 400
const finalWidth = Math.max(minWidth, Math.min(maxWidth, totalWidth))
this.selectWidth = finalWidth + 'px'
// 清理临时元素
document.body.removeChild(tempSpan)
})
},
changeOrg(orgId) {
const org = this.usingOrgs.find(item => item.id === orgId)
@@ -121,6 +180,7 @@ export default {
default:
orgUtil.changeOrg(org, true, this)
}
this.updateWidth()
}
}
}

View File

@@ -0,0 +1,637 @@
<template>
<span ref="root" class="global-search">
<!-- 搜索触发按钮 -->
<div class="search-trigger" @click="openPanel">
<el-input
v-model="search"
:placeholder="$t('Search')"
class="search-input"
readonly
prefix-icon="el-icon-search"
@keydown.esc.prevent="closePanel"
@clear="clearSearch"
>
<template slot="suffix">
<span class="search-shortcut">{{ shortcutText }}</span>
</template>
</el-input>
</div>
<!-- 搜索模态框 -->
<el-dialog
:visible.sync="isOpen"
:close-on-click-modal="true"
:close-on-press-escape="true"
:append-to-body="true"
custom-class="search-modal"
width="70%"
@close="closePanel"
>
<div class="search-modal-content">
<!-- 搜索框 -->
<div class="search-input-wrapper">
<el-input
ref="panelSearchInput"
v-model="search"
:placeholder="$t('Search')"
:clearable="true"
size="large"
prefix-icon="el-icon-search"
@input="onInput"
@keydown.enter.prevent="onEnter"
/>
</div>
<!-- 搜索结果内容 -->
<div class="search-results">
<div v-if="loading" class="section loading">{{ $t('Loading') }}...</div>
<template v-if="showHistory">
<div class="section-title">
<span>{{ $t('History') }}</span>
<el-link class="clear-history-btn" @click="clearHistory">
{{ $t('Clear') }}
</el-link>
</div>
<ul class="list">
<li
v-for="(item, index) in history"
:key="'h-' + index"
class="item"
@click="applyHistory(item)"
>
<i class="el-icon-time icon" />
<span class="label">{{ item.q }}</span>
<i class="el-icon-arrow-right go" />
</li>
</ul>
</template>
<template v-if="routeSuggestions.length">
<div class="section-title">{{ $t('Routes') }}</div>
<ul class="list">
<li
v-for="route in routeSuggestions"
:key="'r-' + route.name + route.path"
class="item"
@click="navigateRoute(route)"
>
<i class="el-icon-location-outline icon" />
<span class="label">{{ route.title || route.name || route.path }}</span>
<span class="sub">{{ route.path }}</span>
</li>
</ul>
</template>
<template v-if="options.length">
<div v-for="group in options" :key="'g-' + group.label" class="section">
<div class="section-title">{{ group.label }}</div>
<ul class="list">
<li
v-for="item in group.options"
:key="item.value"
class="item"
@click="handleSearch(item)"
>
<Icon :icon="iconMap[item.model] || 'el-icon-document'" class="icon" />
<span class="label">{{ item.name }}</span>
<span class="sub">{{ item.content }}</span>
</li>
</ul>
</div>
</template>
<div v-if="search && isEmpty" class="section empty">
{{ $t('NoData') }}
</div>
<div v-if="!search && history.length === 0" class="section placeholder">
<div class="placeholder-content">
<div class="supported-types">
<div class="types-title">{{ $t('SupportedTypes') }}:</div>
<div class="types-list">
<span v-for="(icon, type) in iconMap" :key="type" class="type-item">
<Icon :icon="icon" class="type-icon" />
{{ $t(type) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</span>
</template>
<script>
import _ from 'lodash'
import { mapGetters } from 'vuex'
import { ObjectLocalStorage } from '@/utils/common'
import Icon from '@/components/Widgets/Icon/index.vue'
export default {
name: 'Search',
components: {
Icon
},
data() {
return {
search: '',
loading: false,
options: [],
isOpen: false,
history: [],
routeSuggestions: [],
routes: [],
iconMap: {
'Account': 'accounts',
'Asset': 'assets',
'User': 'user-o',
'UserGroup': 'user-group',
'AssetPermission': 'permission'
},
historyStore: new ObjectLocalStorage('globalSearchHistory')
}
},
computed: {
...mapGetters([
'viewRoutes'
]),
isEmpty() {
return !this.routeSuggestions.length && this.options.length === 0
},
showHistory() {
return this.history.length > 0 && !this.search
},
shortcutText() {
return this.isMac ? '⌘K' : 'Ctrl+K'
},
isMac() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
}
},
mounted() {
this.loadHistory()
this.buildRouteSuggestions()
this.bindKeyboardShortcut()
},
beforeDestroy() {
this.unbindKeyboardShortcut()
},
methods: {
openPanel() {
this.isOpen = true
this.buildRouteSuggestions()
this.$nextTick(() => {
this.$refs.panelSearchInput?.focus()
})
},
closePanel() {
this.isOpen = false
},
onInput() {
this.openPanel()
this.debouncedQuery()
},
clearSearch() {
this.search = ''
this.options = []
this.buildRouteSuggestions()
},
onEnter() {
if (this.options.length > 0) {
this.handleSearch(this.options[0].options[0])
}
},
debouncedQuery: _.debounce(function() {
this.searchQuery(this.search)
}, 300),
async searchQuery(q) {
if (!q) {
this.options = []
return
}
this.loading = true
const url = '/api/v1/search/?q=' + q
try {
const res = await this.$axios.get(url)
let options = res || []
options = _.groupBy(res, 'model_label')
this.options = Object.keys(options).map(key => ({
label: key,
options: options[key]
}))
} catch (error) {
console.error('Search error:', error)
this.options = []
} finally {
this.loading = false
}
},
handleSearch(item) {
const route = {
name: item.model + 'Detail',
params: { id: item.id }
}
this.addToHistory(this.search)
this.$router.push(route)
this.closePanel()
},
navigateRoute(route) {
this.$router.push(route.path)
this.closePanel()
},
filterRouteSuggestions(q) {
if (!q) {
this.routeSuggestions = []
return
}
this.routeSuggestions = this.routes.filter(r => {
const title = r.title || r.name || r.path
return title.toLowerCase().includes(q.toLowerCase()) || r.path.toLowerCase().includes(q.toLowerCase())
}).slice(0, 5)
},
buildRouteSuggestions() {
if (this.routes.length > 0) {
return
}
const allRoutes = this.viewRoutes
const flat = []
const walk = (routes, parentPath = '') => {
for (const r of routes) {
const path = parentPath + r.path
if (r.path && r.path !== '/' && !r.hidden) {
flat.push({
name: r.name,
path: path,
title: r.meta?.title
})
}
if (r.children && r.children.length) {
walk(r.children, path)
}
}
}
walk(allRoutes)
this.routes = flat
},
loadHistory() {
this.history = (this.historyStore.get('list') || []).filter(i => i.q)
},
addToHistory(q) {
const entry = { q: q }
const list = this.historyStore.get('list') || []
const next = [
entry,
...list.filter(i => i.q !== entry.q)
].slice(0, 10)
this.historyStore.set('list', next)
this.history = next
},
applyHistory(h) {
this.search = h.q
this.onInput()
},
clearHistory() {
this.historyStore.set('list', [])
this.history = []
},
bindKeyboardShortcut() {
document.addEventListener('keydown', this.handleKeyboardShortcut)
},
unbindKeyboardShortcut() {
document.removeEventListener('keydown', this.handleKeyboardShortcut)
},
handleKeyboardShortcut(event) {
// 检查是否按下了正确的快捷键
const isCorrectKey = event.key === 'k' || event.key === 'K'
const isCorrectModifier = this.isMac ? event.metaKey : event.ctrlKey
if (isCorrectKey && isCorrectModifier) {
// 阻止默认行为
event.preventDefault()
// 如果当前有输入框聚焦,不触发搜索
const activeElement = document.activeElement
const isInputFocused = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.contentEditable === 'true'
)
if (!isInputFocused) {
this.openPanel()
}
}
}
}
}
</script>
<style lang="scss" scoped>
.global-search {
position: relative;
width: 200px;
height: 40px;
padding: 5px 0;
min-width: 200px;
margin-right: 5px;
.search-trigger {
height: 30px;
line-height: 1;
.search-input {
height: 30px;
line-height: 1;
background-color: rgba(5, 5, 5, 0.1);
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
::v-deep {
.el-input__inner {
height: 30px;
line-height: 1;
background: transparent;
border: none;
color: #fff;
cursor: pointer;
&::placeholder {
color: #fff;
opacity: 0.7;
}
}
.el-input__prefix .el-input__icon {
font-size: 15px;
line-height: 32px;
}
.el-input__suffix {
display: flex;
align-items: center;
height: 100%;
}
}
.search-shortcut {
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 500;
letter-spacing: 0.5px;
padding: 2px 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.2);
user-select: none;
pointer-events: none;
display: inline-flex;
align-items: center;
justify-content: center;
height: 18px;
line-height: 1;
}
}
}
}
/* 搜索模态框全局样式 */
::v-deep .search-modal {
&.el-dialog {
position: fixed;
top: 5px;
left: 50%;
transform: translateX(-50%);
max-height: calc(100vh - 10px);
max-width: calc(100vw - 10px);
border-radius: 5px;
box-shadow: 0 0 8px 4px #00000014;
.el-dialog__body {
padding: 0;
}
.el-dialog__header {
display: none;
}
}
}
::v-deep body .v-modal {
opacity: 0.3;
}
::v-deep .search-modal-content {
height: 70vh;
display: flex;
flex-direction: column;
}
::v-deep .search-input-wrapper {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
// background: #fff;
.el-input {
.el-input__inner {
font-size: 14px;
height: 34px;
line-height: 34px;
}
}
}
::v-deep .search-results {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
}
::v-deep .section-title {
padding: 12px 24px 6px;
font-size: 12px;
line-height: 1.5;
color: #909399;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-top: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
.clear-history-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
color: red;
&:hover {
background: #f5f5f5;
color: #f56c6c;
}
.clear-icon {
font-size: 14px;
color: red;
}
}
}
::v-deep .list {
list-style: none;
margin: 0;
padding: 0;
.item {
display: flex;
align-items: center;
padding: 8px 24px;
cursor: pointer;
border-bottom: 1px solid #f8f9fa;
transition: all 0.2s ease;
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: transparent;
transition: background 0.2s ease;
}
&:hover {
background: #f8f9fa;
}
&:last-child {
border-bottom: none;
}
.icon {
// color: var(--color-primary, #409eff);
margin-right: 12px;
font-size: 14px;
font-weight: 400;
}
.label {
flex: 1;
font-size: 14px;
color: #333;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60%;
}
.sub {
color: #909399;
font-size: 12px;
margin-left: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
width: 40%;
text-align: right;
}
.go {
color: #c0c4cc;
font-size: 12px;
flex-shrink: 0;
}
}
}
::v-deep .loading,
::v-deep .empty {
padding: 32px 24px;
color: #909399;
text-align: center;
font-size: 14px;
}
::v-deep .section.placeholder {
padding: 32px 24px;
.placeholder-content {
text-align: center;
.supported-types {
margin-bottom: 24px;
.types-title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 500;
color: #333;
}
.types-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
.type-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
font-size: 13px;
color: #666;
transition: all 0.2s ease;
&:hover {
background: #e9ecef;
color: #333;
}
.type-icon {
margin-right: 6px;
font-size: 14px;
color: #409eff;
}
}
}
}
}
}
</style>

View File

@@ -1,6 +1,9 @@
<template>
<div class="navbar">
<ul class="navbar-right">
<li class="header-item header-icon none-hover">
<Search @search-open="handleSearchOpen" />
</li>
<li class="header-item header-icon">
<el-tooltip :content="$tc('SiteMessageList')" :open-delay="500" effect="dark">
<SiteMessages />
@@ -31,14 +34,18 @@
<AccountDropdown />
</li>
</ul>
<hamburger :is-active="sidebar.opened" class="hamburger-container is-show-menu" @toggleClick="toggleSideBar" />
<hamburger
:is-active="sidebar.opened"
class="hamburger-container is-show-menu"
@toggleClick="toggleSideBar"
/>
<ul class="navbar-left">
<li class="left-item">
<div class="nav-logo">
<Logo v-if="showLogo" :collapse="false" />
</div>
</li>
<li v-if="orgsShow" class="left-item" style="margin-left: 20px;">
<li v-if="orgsShow" class="left-item" style="margin-left: 20px">
<Organization :disabled="orgsDisabled" class="organization" />
</li>
</ul>
@@ -57,6 +64,7 @@ import Organization from './Organization'
import SystemSetting from './SystemSetting'
import Logo from '../NavLeft/Logo'
import Language from './Language'
import Search from './Search'
export default {
components: {
@@ -69,19 +77,22 @@ export default {
SiteMessages,
SystemSetting,
Logo,
Language
Language,
Search
},
data() {
return {}
return {
searchOpen: false
}
},
computed: {
...mapGetters([
'sidebar', 'publicSettings', 'currentOrgRoles', 'currentViewRoute', 'isMobile'
]),
...mapGetters(['sidebar', 'publicSettings', 'currentOrgRoles', 'currentViewRoute', 'isMobile']),
ticketsEnabled() {
return this.publicSettings['TICKETS_ENABLED'] &&
return (
this.publicSettings['TICKETS_ENABLED'] &&
this.$hasLicense() &&
this.$hasPerm('tickets.view_ticket')
)
},
showLogo() {
return this.$store.state.settings.sidebarLogo
@@ -90,178 +101,188 @@ export default {
return this.$route.meta?.disableOrgsChange === true
},
orgsShow() {
return (this.$route.meta?.showOrganization !== false) && this.$hasLicense()
return this.$route.meta?.showOrganization !== false && this.$hasLicense()
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
handleSearchOpen(val) {
// this.searchOpen = val
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/variables.scss";
@import '~@/styles/variables.scss';
.navbar {
position: relative;
overflow: hidden;
background-color: var(--banner-bg);
.navbar {
position: relative;
overflow: hidden;
background-color: var(--banner-bg);
ul {
margin: 0;
padding-inline-start: 0;
}
ul {
margin: 0;
padding-inline-start: 0;
}
.is-show-menu {
display: none;
}
.is-show-menu {
display: none;
}
.hamburger-container {
float: left;
height: 25px;
line-height: 25px;
margin: 8px;
padding: 1px 8px !important;
border-radius: 5px;
border-color: $--color-primary;
background-color: white;
color: var(--text-primary);
cursor: pointer;
transition: .2s;
opacity: 0.7;
}
.hamburger-container {
float: left;
height: 25px;
line-height: 25px;
margin: 8px;
padding: 1px 8px !important;
border-radius: 5px;
border-color: $--color-primary;
background-color: white;
color: var(--text-primary);
cursor: pointer;
transition: 0.2s;
opacity: 0.7;
}
.navbar-left {
float: left;
.navbar-left {
float: left;
display: flex;
height: 100%;
.left-item {
display: flex;
height: 100%;
.left-item {
display: flex;
align-items: center;
list-style: none;
.nav-logo {
width: 200px;
&:hover {
background: rgba(0, 0, 0, 12%);
}
}
.organization {
display: flex;
align-items: center;
padding: 0 0 0 15px !important;
border-radius: 3px;
background-color: rgba(255, 255, 255, .10);
color: #fff;
font-weight: 600;
font-size: 15px;
max-width: 250px;
::v-deep .el-input__inner {
padding-left: 25px;
}
::v-deep .el-input.is-disabled > input {
background: none;
}
&:hover {
background-color: rgba(0, 0, 0, .12) !important;
}
}
// 未找到与之对应的
& ::v-deep .el-submenu__title {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
padding: 0 8px;
line-height: $headerHeight;
height: $headerHeight;
}
// 未找到与之对应的
& ::v-deep .svg-icon {
color: #FFF !important;
}
}
}
.navbar-right {
display: flex;
float: right;
align-items: center;
margin-right: 10px;
list-style: none;
.header-item {
display: flex;
line-height: $headerHeight;
padding-right: 10px;
padding-left: 10px;
& ::v-deep .svg-icon {
line-height: 40px;
color: #FFF !important;
font-size: 15px;
}
& ::v-deep .el-badge {
vertical-align: top;
.el-link {
vertical-align: baseline;
}
.el-badge__content--primary {
background-color: #fff;
}
.el-badge__content {
top: 8px;
height: 15px;
line-height: 15px;
border: none;
color: var(--color-primary);
}
}
& ::v-deep i {
color: #FFF;
font-size: 16px;
&.el-icon-arrow-down {
font-size: 13px;
}
}
& ::v-deep i.el-dialog__close.el-icon-close {
color: #7c7e7f;
}
.nav-logo {
width: 200px;
&:hover {
background: rgba(0, 0, 0, 12%);
}
}
}
}
@media screen and (max-width: 1006px) {
.navbar {
.is-show-menu {
display: block;
.organization {
display: flex;
align-items: center;
padding: 0 0 0 15px ;
border-radius: 3px;
// background-color: rgba(5, 5, 5, 0.1);
color: #fff;
font-weight: 600;
font-size: 15px;
max-width: 250px;
::v-deep .el-input__inner {
padding-left: 20px;
}
::v-deep .el-input.is-disabled > input {
background: none;
}
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
}
.navbar-left {
display: none;
// 未找到与之对应的
& ::v-deep .el-submenu__title {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
padding: 0 8px;
line-height: $headerHeight;
height: $headerHeight;
}
// 未找到与之对应的
& ::v-deep .svg-icon {
color: #fff !important;
}
}
}
@media screen and (max-width: 480px) {
.active-menu {
display: none !important;;
.navbar-right {
display: flex;
float: right;
align-items: center;
margin-right: 10px;
.header-item {
display: flex;
line-height: $headerHeight;
padding-right: 10px;
padding-left: 10px;
& ::v-deep .svg-icon {
line-height: 40px;
color: #fff;
font-size: 15px;
}
& ::v-deep .el-badge {
vertical-align: top;
.el-link {
vertical-align: baseline;
}
.el-badge__content--primary {
background-color: #fff;
}
.el-badge__content {
top: 8px;
height: 15px;
line-height: 15px;
border: none;
color: var(--color-primary);
}
}
& ::v-deep i {
color: #fff;
font-size: 16px;
&.el-icon-arrow-down {
font-size: 13px;
}
}
& ::v-deep i.el-dialog__close.el-icon-close {
color: #7c7e7f;
}
&.none-hover {
padding: 0;
&:hover {
background: none;
}
}
&:hover {
background: rgba(0, 0, 0, 12%);
}
}
}
}
@media screen and (max-width: 1006px) {
.navbar {
.is-show-menu {
display: block;
}
.navbar-left {
display: none;
}
}
}
@media screen and (max-width: 480px) {
.active-menu {
display: none !important;
}
}
</style>

View File

@@ -1,18 +1,24 @@
<template>
<div :class="{'has-logo': showLogo, 'show-orgs': showOrgs, 'collapsed': isCollapse}" class="left-side-wrapper">
<div
:class="{ 'has-logo': showLogo, 'show-orgs': showOrgs, collapsed: isCollapse }"
class="left-side-wrapper"
>
<div class="nav-header">
<div class="active-mobile">
<Organization v-if="$hasLicense()" class="organization" />
</div>
<div class="nav-title">
<span :class="switchViewOtherClasses" class="switch-view active-switch-view">
<el-popover
:open-delay="200"
placement="right-start"
trigger="hover"
>
<el-popover :open-delay="200" placement="right-start" trigger="hover">
<span slot="reference" style="width: 100%">
<el-tooltip v-show="!isCollapse" :content="isRouteMeta.title" :open-delay="1000" placement="bottom" effect="dark" class="view-title">
<el-tooltip
v-show="!isCollapse"
:content="isRouteMeta.title"
:open-delay="1000"
placement="bottom"
effect="dark"
class="view-title"
>
<span class="text-overflow">{{ isRouteMeta.title || '' }}</span>
</el-tooltip>
<span class="icon-zone">
@@ -49,10 +55,14 @@
</div>
<div class="nav-footer">
<div class="toggle-bar">
<Hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<Hamburger
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
</div>
</div>
<div :class="{'is-show': viewShown}" class="mobile-menu" @click="viewShown = false">
<div :class="{ 'is-show': viewShown }" class="mobile-menu" @click="viewShown = false">
<ViewSwitcher :mode="'vertical'" />
</div>
</div>
@@ -81,10 +91,7 @@ export default {
}
},
computed: {
...mapGetters([
'currentViewRoute',
'sidebar'
]),
...mapGetters(['currentViewRoute', 'sidebar']),
defaultOpensMenu() {
return []
},
@@ -127,8 +134,7 @@ export default {
return this.currentViewRoute.meta || {}
}
},
mounted() {
},
mounted() {},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
@@ -143,7 +149,9 @@ export default {
}, 500)
},
setLeastMenuOpen() {
const hasOpened = document.querySelector('.el-submenu-sidebar.submenu-item.el-submenu.is-opened')
const hasOpened = document.querySelector(
'.el-submenu-sidebar.submenu-item.el-submenu.is-opened'
)
if (hasOpened) {
return
}
@@ -156,7 +164,7 @@ export default {
}
</script>
<style lang="scss" scoped>
@import "~@/styles/variables.scss";
@import '~@/styles/variables.scss';
$mobileHeight: 40px;
$origin-color: #ffffff;
@@ -235,7 +243,7 @@ $hover-border-color: #d2d2d2;
.view-title {
width: calc(100% - 10px);
display: inline-block
display: inline-block;
}
.icon-zone {
@@ -299,7 +307,7 @@ $hover-border-color: #d2d2d2;
}
.is-show {
display: block !important;;
display: block !important;
}
.mobile-menu {

View File

@@ -167,8 +167,7 @@ export default {
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.page.no-title {
::v-deep {
.page-submenu .el-tabs__header {
@@ -199,6 +198,14 @@ export default {
opacity: 1;
}
}
&.is-disabled {
cursor: not-allowed;
&:hover {
color: #c0c4cc;
}
}
}
.el-tabs__nav-next {

View File

@@ -276,10 +276,8 @@ export default {
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
// reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {

View File

@@ -1,5 +1,5 @@
import Vue from 'vue'
import ElementUI from 'elementui-lts'
import ElementUI from 'element-ui'
import locale from 'elementLocale'
import '@/styles/index.scss' // global css
import App from './App'
@@ -25,6 +25,7 @@ import xss from '@/utils/secure'
import ElTableTooltipPatch from '@/utils/vue/elTableTooltipPatch.js'
import VSanitize from 'v-sanitize'
import moment from 'moment'
moment.locale('zh-cn')
/**
@@ -35,10 +36,11 @@ moment.locale('zh-cn')
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
if (process.env.NODE_ENV === 'development') {
const { mockXHR } = require('../mock')
mockXHR()
}
// 使用 mockXHR 无法使用 axios 中的 onUploadProgress 回调函数
// if (process.env.NODE_ENV === 'development') {
// const { mockXHR } = require('../mock')
// mockXHR()
// }
// set ElementUI lang to EN
ElementUI.Tooltip.props.openDelay.default = 1000

View File

@@ -90,7 +90,8 @@ export default {
meta: {
title: i18n.t('Report'),
icon: 'report',
permissions: []
permissions: [],
licenseRequired: true
},
children: ReportsRoutes
}

View File

@@ -18,7 +18,7 @@ export default [
component: () => import('@/views/reports/users/index.vue'),
meta: {
title: i18n.t('UserReport'),
permissions: []
permissions: ['rbac.view_userloginreport | rbac.view_userchangepasswordreport']
}
}
]
@@ -40,7 +40,7 @@ export default [
component: () => import('@/views/reports/assets/index.vue'),
meta: {
title: i18n.t('AssetReport'),
permissions: []
permissions: ['rbac.view_assetactivityreport | rbac.view_assetstatisticsreport']
}
}
]
@@ -62,7 +62,7 @@ export default [
component: () => import('@/views/reports/accounts/index.vue'),
meta: {
title: i18n.t('AccountReport'),
permissions: []
permissions: ['rbac.view_accountstatisticsreport | rbac.view_accountautomationreport']
}
}
]

View File

@@ -22,6 +22,7 @@ export default [
component: () => import('@/views/accounts/Account/index.vue'),
meta: {
title: i18n.t('AssetAccount'),
showInSearch: true,
app: 'accounts',
permissions: ['accounts.view_account']
}
@@ -29,8 +30,8 @@ export default [
{
path: ':id',
component: () => import('@/views/accounts/Account/AccountDetail/index.vue'),
name: 'AssetAccountDetail',
meta: { title: i18n.t('AssetAccountDetail') },
name: 'AccountDetail',
meta: { title: i18n.t('AccountDetail') },
hidden: true
}
]

View File

@@ -34,7 +34,9 @@ export default [
name: 'UserLoginACLList',
component: () => import('@/views/acls/UserLoginACL/UserLoginACLList.vue'),
meta: {
title: i18n.t('UserLoginACLs'), menuTitle: i18n.t('UserLogin'), activeMenu: ''
title: i18n.t('UserLoginACLs'),
menuTitle: i18n.t('UserLogin'),
activeMenu: ''
}
},
{
@@ -74,7 +76,7 @@ export default [
},
name: 'CmdACL',
meta: {
title: i18n.t('CommandFilterACLs',),
title: i18n.t('CommandFilterACLs'),
menuTitle: i18n.t('CommandFilter'),
app: 'acls',
resource: 'commandfilteracl'
@@ -136,7 +138,11 @@ export default [
path: '',
name: 'AssetACLList',
component: () => import('@/views/acls/AssetLoginACL/AssetLoginAclList.vue'),
meta: { title: i18n.t('AssetACLs'), activeMenu: '', menuTitle: i18n.t('AssetConnect') }
meta: {
title: i18n.t('AssetACLs'),
activeMenu: '',
menuTitle: i18n.t('AssetConnect')
}
},
{
path: 'create',
@@ -161,6 +167,53 @@ export default [
}
]
},
{
path: 'data-masking-rules',
component: empty,
redirect: {
name: 'DataMaskingRuleList'
},
name: 'DataMaskingRules',
meta: {
title: i18n.t('DataMasking'),
licenseRequired: true,
app: 'acls',
resource: 'datamaskingrule'
},
children: [
{
path: '',
name: 'DataMaskingRuleList',
component: () => import('@/views/acls/DataMaskingRule/DataMaskingRuleList.vue'),
meta: {
title: i18n.t('DataMasking'),
activeMenu: '',
menuTitle: i18n.t('DataMasking')
}
},
{
path: 'create',
name: 'DataMaskingRuleCreate',
component: () => import('@/views/acls/DataMaskingRule/DataMaskingRuleCreateUpdate.vue'),
hidden: true,
meta: { title: '', activeMenu: '' }
},
{
path: ':id',
name: 'DataMaskingRuleDetail',
component: () => import('@/views/acls/DataMaskingRule/DataMaskingRuleDetail/index'),
hidden: true,
meta: { title: i18n.t('AssetACLDetail'), activeMenu: '' }
},
{
path: ':id/update',
name: 'DataMaskingRuleUpdate',
component: () => import('@/views/acls/DataMaskingRule/DataMaskingRuleCreateUpdate.vue'),
hidden: true,
meta: { title: '', activeMenu: '' }
}
]
},
{
path: 'cmd-groups',
component: empty,
@@ -237,7 +290,11 @@ export default [
path: '',
name: 'ConnectMethodACLList',
component: () => import('@/views/acls/ConnectMethodACL/ConnectMethodAclList.vue'),
meta: { title: i18n.t('ConnectMethodACLs'), activeMenu: '', menuTitle: i18n.t('ConnectMethod') }
meta: {
title: i18n.t('ConnectMethodACLs'),
activeMenu: '',
menuTitle: i18n.t('ConnectMethod')
}
},
{
path: 'create',

View File

@@ -20,7 +20,7 @@ export default [
path: '',
name: 'AssetList',
component: () => import('@/views/assets/Asset/AssetList/index.vue'),
meta: { title: i18n.t('AssetList') }
meta: { title: i18n.t('AssetList'), showInSearch: true }
},
{
path: ':id',

View File

@@ -22,6 +22,7 @@ export default {
showNavSwitcher: () => {
return store.getters.consoleOrgs.length > 0
},
permissions: []
},
children: [

View File

@@ -34,14 +34,12 @@ export default [
{
path: `${BASE_URL}/luna/?_=${Date.now()}`,
name: 'WebTerminal',
// component: () => window.open(`/luna/?_=${Date.now()}`),
meta: { title: i18n.t('WebTerminal') }
// hidden: true
},
{
path: `${BASE_URL}/koko/elfinder/sftp/?`,
name: 'FileManagement',
// component: () => window.open(`/koko/elfinder/sftp/?`),
meta: { title: i18n.t('FileManagement') }
}
]

View File

@@ -6,7 +6,7 @@ export default [
component: () => import('@/views/reports/accounts/AccountStatistics.vue'), // Parent router-view
name: 'AccountStatistics',
meta: {
permissions: [],
permissions: ['rbac.view_accountstatisticsreport'],
expanded: true,
icon: 'user-o',
title: i18n.t('AccountStatisticsReport')
@@ -17,7 +17,7 @@ export default [
component: () => import('@/views/reports/accounts/AccountAutomation.vue'), // Parent router-view
name: 'AccountAutomationReport',
meta: {
permissions: [],
permissions: ['rbac.view_accountautomationreport'],
expanded: true,
icon: 'user-o',
title: i18n.t('AccountAutomationReport')

View File

@@ -6,7 +6,7 @@ export default [
component: () => import('@/views/reports/assets/AssetActivity.vue'), // Parent router-view
name: 'AssetReport',
meta: {
permissions: [],
permissions: ['rbac.view_assetactivityreport'],
icon: 'user-o',
expanded: true,
title: i18n.t('AssetActivityReport')
@@ -17,7 +17,7 @@ export default [
component: () => import('@/views/reports/assets/AssetStatistics.vue'), // Parent router-view
name: 'AssetStatistics',
meta: {
permissions: [],
permissions: ['rbac.view_assetstatisticsreport'],
expanded: true,
icon: 'user-o',
title: i18n.t('AssetStatisticsReport')

View File

@@ -0,0 +1,48 @@
import i18n from '@/i18n/i18n'
export default [
{
path: 'console',
component: () => import('@/views/reports/console/index.vue'), // Parent router-view
name: 'ConsoleReport',
meta: {
permissions: ['rbac.view_console'],
icon: 'user-o',
expanded: true,
title: i18n.t('ConsoleReport')
}
},
{
path: 'pam',
component: () => import('@/views/reports/pam/Dashboard/index.vue'), // Parent router-view
name: 'PamReport',
meta: {
permissions: ['rbac.view_pam'],
expanded: true,
icon: 'user-o',
title: i18n.t('PamReport')
}
},
{
path: 'audits',
component: () => import('@/views/reports/audits/Dashboard.vue'), // Parent router-view
name: 'AuditsReport',
meta: {
permissions: ['rbac.view_audit'],
expanded: true,
icon: 'user-o',
title: i18n.t('AuditsReport')
}
},
{
path: 'change-secret',
component: () => import('@/views/reports/pam/ChangeSecret/index.vue'),
name: 'ChangeSecretReport',
meta: {
permissions: ['accounts.view_changesecretautomation'],
expanded: true,
icon: 'user-o',
title: i18n.t('ChangeSecretReport')
}
}
]

View File

@@ -4,6 +4,7 @@ import empty from '@/layout/empty'
import users from './users'
import assets from './assets'
import accounts from './accounts'
import dashboard from './dashboard'
export default {
path: '/reports',
@@ -17,6 +18,18 @@ export default {
permissions: []
},
children: [
{
path: '/reports/dashboard',
component: empty,
redirect: 'dashboard',
name: 'ReportsDashboard',
meta: {
title: i18n.t('ReportsDashboard'),
icon: 'reports',
permissions: []
},
children: dashboard
},
{
path: '/reports/users',
component: empty,

View File

@@ -6,7 +6,7 @@ export default [
component: () => import('@/views/reports/users/UserActivity.vue'), // Parent router-view
name: 'UserReport',
meta: {
permissions: [],
permissions: ['rbac.view_userloginreport'],
icon: 'user-o',
expanded: true,
title: i18n.t('UserLoginReport')
@@ -17,7 +17,7 @@ export default [
component: () => import('@/views/reports/users/ChangePassword.vue'), // Parent router-view
name: 'ChangePassword',
meta: {
permissions: [],
permissions: ['rbac.view_userchangepasswordreport'],
expanded: true,
icon: 'user-o',
title: i18n.t('UserChangePasswordReport')

View File

@@ -6,19 +6,21 @@
/* Transition
-------------------------- */
$--all-transition: all .3s cubic-bezier(.645,.045,.355,1) !default;
$--all-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
$--fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--fade-linear-transition: opacity 200ms linear !default;
$--md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--border-transition-base: border-color .2s cubic-bezier(.645,.045,.355,1) !default;
$--color-transition-base: color .2s cubic-bezier(.645,.045,.355,1) !default;
$--md-fade-transition:
transform 300ms cubic-bezier(0.23, 1, 0.32, 1),
opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--border-transition-base: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
$--color-transition-base: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
/* Color
-------------------------- */
/// color|1|Brand Color|0
$--color-primary: #1ab394 !default;
/// color|1|Background Color|4
$--color-white: #FFFFFF !default;
$--color-white: #ffffff !default;
/// color|1|Background Color|4
$--color-black: #000000 !default;
$--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */
@@ -55,19 +57,19 @@ $--color-text-regular: #606266 !default;
/// color|1|Font Color|2
$--color-text-secondary: #909399 !default;
/// color|1|Font Color|2
$--color-text-placeholder: #C0C4CC !default;
$--color-text-placeholder: #c0c4cc !default;
/// color|1|Border Color|3
$--border-color-base: #DCDFE6 !default;
$--border-color-base: #dcdfe6 !default;
/// color|1|Border Color|3
$--border-color-light: #E4E7ED !default;
$--border-color-light: #e4e7ed !default;
/// color|1|Border Color|3
$--border-color-lighter: #EBEEF5 !default;
$--border-color-lighter: #ebeef5 !default;
/// color|1|Border Color|3
$--border-color-extra-light: #F2F6FC !default;
$--border-color-extra-light: #f2f6fc !default;
// Background
/// color|1|Background Color|4
$--background-color-base: #F5F7FA !default;
$--background-color-base: #f5f7fa !default;
/* Link
-------------------------- */
@@ -91,9 +93,13 @@ $--border-radius-zero: 0 !default;
// Box-shadow
/// boxShadow|1|Shadow|1
$--box-shadow-base: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04) !default;
$--box-shadow-base:
0 2px 4px rgba(0, 0, 0, 0.12),
0 0 6px rgba(0, 0, 0, 0.04) !default;
// boxShadow|1|Shadow|1
$--box-shadow-dark: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .12) !default;
$--box-shadow-dark:
0 2px 4px rgba(0, 0, 0, 0.12),
0 0 6px rgba(0, 0, 0, 0.12) !default;
/// boxShadow|1|Shadow|1
$--box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !default;
@@ -210,8 +216,6 @@ $--checkbox-button-checked-font-color: $--color-white !default;
/// color||Color|0
$--checkbox-button-checked-border-color: $--color-primary !default;
/* Radio
-------------------------- */
/// fontSize||Font|1
@@ -480,8 +484,8 @@ $--cascader-menu-radius: $--border-radius-base !default;
$--cascader-menu-border: solid 1px $--border-color-light !default;
$--cascader-menu-shadow: $--box-shadow-light !default;
$--cascader-node-background-hover: $--background-color-base !default;
$--cascader-node-color-disabled:$--color-text-placeholder !default;
$--cascader-color-empty:$--color-text-placeholder !default;
$--cascader-node-color-disabled: $--color-text-placeholder !default;
$--cascader-color-empty: $--color-text-placeholder !default;
$--cascader-tag-background: #f0f2f5;
/* Group
@@ -591,7 +595,6 @@ $--button-info-background-color: $--color-info !default;
$--button-hover-tint-percent: 20% !default;
$--button-active-shade-percent: 10% !default;
/* cascader
-------------------------- */
$--cascader-height: 200px !default;
@@ -639,7 +642,7 @@ $--table-row-hover-background-color: $--background-color-base !default;
$--table-current-row-background-color: $--color-primary-light-9 !default;
/// color||Color|0
$--table-header-background-color: $--color-white !default;
$--table-fixed-box-shadow: 0 0 10px rgba(0, 0, 0, .12) !default;
$--table-fixed-box-shadow: 0 0 10px rgba(0, 0, 0, 0.12) !default;
/* Pagination
-------------------------- */
@@ -827,8 +830,8 @@ $--loading-fullscreen-spinner-size: 50px !default;
/* Scrollbar
--------------------------*/
$--scrollbar-background-color: rgba($--color-text-secondary, .3) !default;
$--scrollbar-hover-background-color: rgba($--color-text-secondary, .5) !default;
$--scrollbar-background-color: rgba($--color-text-secondary, 0.3) !default;
$--scrollbar-hover-background-color: rgba($--color-text-secondary, 0.5) !default;
/* Carousel
--------------------------*/
@@ -937,7 +940,7 @@ $--link-info-font-color: $--color-info !default;
/// border||Other|4
$--calendar-border: $--table-border !default;
/// color||Other|4
$--calendar-selected-background-color: #F2F8FE !default;
$--calendar-selected-background-color: #f2f8fe !default;
$--calendar-cell-width: 85px !default;
/* Form
@@ -950,7 +953,7 @@ $--form-label-font-size: $--font-size-base !default;
/// color||Color|0
$--avatar-font-color: #fff !default;
/// color||Color|0
$--avatar-background-color: #C0C4CC !default;
$--avatar-background-color: #c0c4cc !default;
/// fontSize||Font Size|1
$--avatar-text-font-size: 14px !default;
/// fontSize||Font Size|1
@@ -972,26 +975,52 @@ $--lg: 1200px !default;
$--xl: 1920px !default;
$--breakpoints: (
'xs' : (max-width: $--sm - 1),
'sm' : (min-width: $--sm),
'md' : (min-width: $--md),
'lg' : (min-width: $--lg),
'xl' : (min-width: $--xl)
'xs': (
max-width: $--sm - 1
),
'sm': (
min-width: $--sm
),
'md': (
min-width: $--md
),
'lg': (
min-width: $--lg
),
'xl': (
min-width: $--xl
)
);
$--breakpoints-spec: (
'xs-only' : (max-width: $--sm - 1),
'sm-and-up' : (min-width: $--sm),
'sm-only': "(min-width: #{$--sm}) and (max-width: #{$--md - 1})",
'sm-and-down': (max-width: $--md - 1),
'md-and-up' : (min-width: $--md),
'md-only': "(min-width: #{$--md}) and (max-width: #{$--lg - 1})",
'md-and-down': (max-width: $--lg - 1),
'lg-and-up' : (min-width: $--lg),
'lg-only': "(min-width: #{$--lg}) and (max-width: #{$--xl - 1})",
'lg-and-down': (max-width: $--xl - 1),
'xl-only' : (min-width: $--xl),
'xs-only': (
max-width: $--sm - 1
),
'sm-and-up': (
min-width: $--sm
),
'sm-only': '(min-width: #{$--sm}) and (max-width: #{$--md - 1})',
'sm-and-down': (
max-width: $--md - 1
),
'md-and-up': (
min-width: $--md
),
'md-only': '(min-width: #{$--md}) and (max-width: #{$--lg - 1})',
'md-and-down': (
max-width: $--lg - 1
),
'lg-and-up': (
min-width: $--lg
),
'lg-only': '(min-width: #{$--lg}) and (max-width: #{$--xl - 1})',
'lg-and-down': (
max-width: $--xl - 1
),
'xl-only': (
min-width: $--xl
)
);
$--font-path: '~elementui-lts/lib/theme-chalk/fonts';
@import "~elementui-lts/packages/theme-chalk/src/index";
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';

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