Compare commits

...

450 Commits

Author SHA1 Message Date
fit2bot
f9f40cb3c9 feat: Update v3.10.11 2024-06-19 19:37:28 +08:00
w940853815
93ea7f034a Revert "fix: 待我审批列表页面默认过滤出还未审批的工单"
This reverts commit 91c44d0500.
2024-06-19 19:34:39 +08:00
Bryan
0a3dc30c85 Merge pull request #4072 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:04:12 +08:00
wangruidong
9c8ceb04f0 perf: Disable editing labels for the root organization 2024-06-19 15:46:46 +08:00
wangruidong
ccd7b319c8 fix: 审计台仪表盘今日数据有问题 2024-06-18 17:59:18 +08:00
feng626
55637c7fa1 Merge pull request #4063 from jumpserver/pr@dev@account_template
fix: Account tempalte update secret failed
2024-06-18 14:54:14 +08:00
feng
4e95c88318 fix: Account tempalte update secret failed 2024-06-18 14:52:50 +08:00
wangruidong
1ff49ca16d perf: 系统设置-组织管理:最新创建成功后返回列表未按创建时间排序 2024-06-17 17:30:34 +08:00
wangruidong
91c44d0500 fix: 待我审批列表页面默认过滤出还未审批的工单 2024-06-13 16:15:35 +08:00
wangruidong
0b3a9844f7 fix: Crontab - Failed to set minute range 2024-06-13 10:40:22 +08:00
feng
95b58f3c96 perf: Complete asset hardware information 2024-06-11 18:49:03 +08:00
wangruidong
128b9c79ba fix: 工单-待我审批默认筛选打开的工单 2024-06-11 17:46:09 +08:00
halo
4eda83f83d perf: 优化图标不对齐的问题 2024-06-07 15:55:20 +08:00
halo
4cd0071054 feat: 支持批量测试资产可连接性 2024-06-07 11:18:35 +08:00
wangruidong
67a2a9be6a If non-existent values are entered into the select component, won't trigger a search request. 2024-06-05 16:01:46 +08:00
ibuler
f927a2a3cc perf: action 添加 token 2024-06-04 10:32:48 +08:00
Gerry.tan
ca40cb34da feat: 新增 dameng 图标 2024-05-31 11:11:06 +08:00
feng
d725e5497d perf: console dashboard api 2024-05-21 16:26:34 +08:00
Bryan
51d24bc8e5 Merge pull request #3941 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:05:04 +08:00
ibuler
56f6c17275 perf: 优先选择上个 org 切换 2024-05-16 13:53:47 +08:00
ibuler
e1bde89b29 fix: 修复切换到全局组织回不来的问题
perf: 修改组织切换
2024-05-15 19:14:58 +08:00
feng626
e9da168c9f Merge pull request #3930 from jumpserver/pr@dev@mfa
perf: User personal settings mfa new window opens
2024-05-15 16:41:15 +08:00
feng
c19ef24ec9 perf: User personal settings mfa new window opens 2024-05-15 16:40:17 +08:00
wangruidong
fb7c4a8b2a fix: 竖屏审批工单时,动作显示不出来 2024-05-15 15:33:03 +08:00
wangruidong
428ba49f9c perf: 根据type生成导出文件名 2024-05-11 10:02:53 +08:00
ibuler
7602d6e270 fix: 修复自动切换到 root org 回不来的问题 2024-05-10 19:07:46 +08:00
wangruidong
7b62ce2d33 fix: 批量传输下载结果文件名undefined 2024-05-07 14:42:06 +08:00
jiangweidong
6ed40c45b0 perf: 云同步支持同步完成后自动删除云端已经释放的资产 2024-05-07 14:40:29 +08:00
wangruidong
31238e0398 fix: 命令存储创建es后跳转路由不对 2024-04-28 10:24:44 +08:00
wangruidong
676ac2bbf6 perf: 创建、更新用户时MFA选项根据系统设置选项进行动态渲染 2024-04-26 11:35:12 +08:00
Bai
d5415b84c9 feat: Support asset tree node drag to another one 2024-04-24 18:06:12 +08:00
Bai
5e91917ba4 perf: 优化 Web 资产详情时根据 autofill 类型返回对应的 spec_info 信息 2024-04-23 13:08:59 +08:00
zhaojisen
c4361b4c17 perf: 修复默认值相关内容,优化按钮禁用条件 2024-04-23 10:17:43 +08:00
Bryan
1b15a4d043 Merge pull request #3871 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:44:33 +08:00
Bai
70b5ec3683 fix: Fixed User first login long wait.(SYSTEM ORG) 2024-04-22 19:17:11 +08:00
Bai
c93a061852 fix: Linux Platform id is not 1, create gateway error 2024-04-22 15:54:56 +08:00
feng626
4351d20a1e Merge pull request #3867 from jumpserver/pr@dev@asset
perf: 新建/更新资产时,新tab页面打开
2024-04-22 14:17:29 +08:00
feng
ce23d53e3c perf: 新建/更新资产时,新tab页面打开 2024-04-22 14:16:59 +08:00
wangruidong
32fc16126f perf: dashboard typo 2024-04-22 13:24:52 +08:00
Bryan
7d3f818242 Merge pull request #3864 from jumpserver/v3.10
v3.10.8
2024-04-18 17:58:05 +08:00
Bryan
4e26f18d77 Merge pull request #3862 from jumpserver/dev
v3.10.8
2024-04-18 17:17:36 +08:00
wangruidong
b9a99148e3 perf: 修改用户详情页-资产授权规则默认字段展示 2024-04-18 15:47:42 +08:00
ibuler
2a4b99484b perf: 修改查看 account 密码,切换 route 查询多次 2024-04-18 13:39:45 +08:00
ibuler
bd26894135 perf: 优化查看 account secret api 次数 2024-04-18 13:29:20 +08:00
wangruidong
01e55d7f6e perf: 全局组织禁用资产的测试可连接性 2024-04-18 11:40:10 +08:00
wangruidong
d0a7201683 perf: 优化查看资产账号列表过长显示不全 2024-04-18 11:25:52 +08:00
feng626
e2dcc98ab3 Merge pull request #3856 from jumpserver/pr@dev@dashboard
perf: dashboard 数字保留小数后两位
2024-04-17 20:05:45 +08:00
feng
8dd4f89395 perf: dashboard 数字保留小数后两位 2024-04-17 20:04:53 +08:00
feng
33a997f3bb fix: 仪表盘计算不准确 2024-04-17 19:16:59 +08:00
wangruidong
1adee78456 perf: QuickJob asset tree css 2024-04-17 14:45:12 +08:00
ibuler
baf2b6cd9b perf: view secret request many times 2024-04-17 14:25:36 +08:00
jiangweidong
28e756e163 perf: 优化云同步测试账号再次打开后不显示上次选的地域信息 2024-04-16 17:44:17 +08:00
老广
251873a7e9 Merge pull request #3850 from jumpserver/pr@dev@perf_krry_page
perf: 优化 krry paging
2024-04-16 17:33:20 +08:00
wangruidong
ff9bd42322 perf: 适配手机端 2024-04-16 15:40:30 +08:00
ibuler
2c245020cd perf: 优化 krry paging
chrome: remove debug
2024-04-16 14:34:20 +08:00
wangruidong
429d2fec40 perf: asset tree css 2024-04-16 11:38:40 +08:00
feng626
c2f8fe45a1 Merge pull request #3848 from jumpserver/pr@dev@records
fix: 改密推送任务记录查看失败
2024-04-16 10:36:08 +08:00
feng
caa5e7df75 fix: 改密推送任务记录查看失败 2024-04-16 10:34:05 +08:00
ibuler
8a439dec8d fix: 修复先删除标签resource,再创建报错 2024-04-15 19:43:51 +08:00
ibuler
eabf5a117d perf: account template support labels 2024-04-15 19:43:31 +08:00
wangruidong
c30672a014 fix: 会话详情中文件传输显示有误 2024-04-15 14:42:40 +08:00
wangruidong
5c5df32181 perf: 优化停止任务log输出 2024-04-12 11:29:41 +08:00
Bai
ce831df717 fix: I18n 2024-04-12 11:20:26 +08:00
feng626
04688930ad Merge pull request #3841 from jumpserver/pr@dev@change_secret
fix: 改密任务记录搜索失败
2024-04-12 11:03:01 +08:00
feng
ae5787ae52 fix: 改密任务记录搜索失败 2024-04-12 11:01:39 +08:00
wangruidong
1dfdfe3932 fix: xss处理后无class属性 2024-04-11 19:23:52 +08:00
wangruidong
f409abbf79 perf: 修改按钮样式,添加网关label 2024-04-11 17:33:55 +08:00
wangruidong
a6232da3d0 perf: 修改任务停止日志输出提示 2024-04-11 14:40:35 +08:00
feng626
2690538db6 Merge pull request #3837 from jumpserver/pr@dev@logo
fix: 点击logo前端页面卡住
2024-04-11 14:38:35 +08:00
feng
6d0af7a149 fix: 点击logo前端页面卡住 2024-04-11 14:32:05 +08:00
Bai
babc048eb0 fix: Session duration recurring 2024-04-11 11:01:36 +08:00
wangruidong
d49be903e8 perf: Domain detail add gateways 2024-04-10 16:46:47 +08:00
Bai
332058b0ea perf: Domain detail add asset 2024-04-10 16:46:47 +08:00
feng
e355abc1af perf: 切换zh hant 2024-04-10 15:31:39 +08:00
Bai
99200d58bb feat: LDAP User Auth support cache user_dn 2024-04-09 20:11:06 +08:00
ibuler
61bb97efa9 perf: xss attack 2024-04-09 17:09:01 +08:00
fit2bot
c5e030e2fe perf: Update user orgs roles (#3829)
* perf: Update user orgs roles

* perf: Update user orgs roles

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
2024-04-09 16:51:05 +08:00
fit2bot
8a60ad774f perf: Update user orgs roles (#3828)
Co-authored-by: Bai <baijiangjie@gmail.com>
2024-04-09 15:44:07 +08:00
hzhfit2cloud
b08e6de527 支持中文繁体 2024-04-09 15:20:39 +08:00
feng
a96851686f fix: 修复xss漏洞 工单查不到数据问题 2024-04-08 18:56:22 +08:00
wangruidong
1b0adbfe71 fix: Web资产克隆,选择器信息丢失 2024-04-08 18:54:43 +08:00
wangruidong
c28419438b fix: 虚拟账号列表排序无效 2024-04-08 16:34:10 +08:00
jiangweidong
8807f24bcd perf: 云同步支持火山引擎 2024-04-08 14:04:56 +08:00
Bai
b8a914eb02 perf: ROOT Org show orgs-and-roles in user-detail page 2024-04-08 14:02:10 +08:00
wangruidong
8bbc66a281 fix: 授权资产查看账号失败 2024-04-08 11:35:13 +08:00
feng626
19bab778ca Merge pull request #3818 from jumpserver/pr@dev@user_session
fix: 修复用户下线失败问题
2024-04-03 16:41:43 +08:00
feng
134bcda895 fix: 修复用户下线失败问题 2024-04-03 16:14:54 +08:00
wangruidong
8b725fa4f6 perf: TransferSelect Dialog手机端适配 2024-04-02 19:21:33 +08:00
wangruidong
205f8bc280 fix: 勾选指定账号时,数量显示有问题 2024-04-02 19:20:44 +08:00
wangruidong
4963446b74 fix: 统一成模板 2024-04-02 19:19:40 +08:00
wangruidong
98be3903db fix: 华为交换机执行快捷命令报错 2024-04-02 18:51:19 +08:00
wangruidong
2cb7a859a8 perf: 授权的资产列表中支持查看某个资产授权的账号 2024-04-02 17:06:55 +08:00
wangruidong
d51571c530 perf: 搜索框和右侧在同一水平线上 2024-03-29 18:29:50 +08:00
wangruidong
bf0d19ac0b perf: 工单页面去掉一些筛选字段 2024-03-29 14:17:44 +08:00
wangruidong
28062f5d60 perf: Web,ChatGPT资产测试可连接性按钮禁用 2024-03-28 18:20:35 +08:00
ibuler
720dee578a fix: 修复创建账号时,资产没有默认值的问题 2024-03-28 18:15:50 +08:00
feng626
cc0d78a4e7 Merge pull request #3809 from jumpserver/pr@dev@role
fix: 修改content type 权限
2024-03-28 15:22:13 +08:00
feng
9f0b904043 fix: 修改content type 权限 2024-03-28 15:21:31 +08:00
dependabot[bot]
90cbb25d47 build(deps): bump express from 4.18.2 to 4.19.2
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 16:18:44 +08:00
dependabot[bot]
450d9562c3 build(deps): bump ip from 1.1.8 to 1.1.9
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 16:18:19 +08:00
dependabot[bot]
a69eec5ef6 build(deps): bump axios from 0.21.1 to 0.28.0
Dependabot couldn't find the original pull request head commit, a01b1986d1e53870ecc18abd7ee72104fba7a08b.
2024-03-27 16:17:56 +08:00
Bryan
b22613617a Revert "build(deps): bump follow-redirects from 1.15.3 to 1.15.4"
This reverts commit e971cbf4a8.
2024-03-27 16:16:07 +08:00
dependabot[bot]
e971cbf4a8 build(deps): bump follow-redirects from 1.15.3 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-27 16:11:26 +08:00
wangruidong
5f6fc7e3b4 fix: 待审批列表显示没有过滤的问题 2024-03-27 16:10:22 +08:00
wangruidong
62b6ca026e perf: 优化资产、网域、网关的操作体验 2024-03-27 16:02:39 +08:00
Bai
006f258938 perf: Optimize the context menu for right-clicking on the asset tree. 2024-03-27 10:56:47 +08:00
feng626
756b7db6b6 Merge pull request #3798 from jumpserver/pr@dev@perm_asset
perf: 资产授权列表优化
2024-03-26 18:07:51 +08:00
feng
a8d7c01f94 perf: 资产授权列表优化 2024-03-26 18:06:37 +08:00
feng
0c7e7ecc99 feat: 拆分 feishu lark 2024-03-26 17:11:22 +08:00
feng626
342a70c441 Merge pull request #3795 from jumpserver/pr@dev@asset_perm
fix: 资产授权列表没有有效期字段
2024-03-22 16:01:12 +08:00
wangruidong
815247f5b5 perf: 封装SwitchFormatter组件 2024-03-22 15:28:13 +08:00
wangruidong
e6721a9905 feat: 支持开启、关闭定时任务执行 2024-03-22 15:28:13 +08:00
feng
d6305fddfd fix: 资产授权列表没有有效期字段 2024-03-22 14:47:15 +08:00
Eric
6db2d5ae31 perf: 支持发布机卸载远程应用 2024-03-21 16:12:13 +08:00
feng626
9b43bba6f8 Merge pull request #3790 from jumpserver/pr@dev@change_secret_record
feat: 改密记录可批量重试 新增更多过滤选项
2024-03-21 11:09:26 +08:00
feng
f9d89b30e2 feat: 改密记录可批量重试 新增更多过滤选项 2024-03-21 11:08:30 +08:00
feng626
be29794b4c Merge pull request #3788 from jumpserver/pr@dev@change_secret
perf: 改密记录可查看密文
2024-03-21 11:05:11 +08:00
jiangweidong
97bf4f1b97 feat: 云账号测试支持选择地域 2024-03-20 16:39:12 +08:00
Bai
76e7684e26 perf: 优化 Session 支持 duration 字段 2024-03-20 15:52:23 +08:00
wangruidong
2bfd764da7 perf: 支持导出收集账号列表的数据 2024-03-18 18:02:58 +08:00
wangruidong
daf3defe14 perf: 修改oracle默认数据库提示 2024-03-18 18:02:34 +08:00
Eric
d8c165ca78 perf: 支持发布机仅初始化配置 2024-03-18 15:52:22 +08:00
feng
42115b2c30 perf: 改密记录可查看密文 2024-03-15 17:29:23 +08:00
老广
447013abf5 Merge pull request #3784 from jumpserver/pr@dev@perf_asset_tree
perf: 优化资产树 root 节点宽度
2024-03-15 14:43:24 +08:00
ibuler
44216326f9 perf: 优化资产树 root 节点宽度 2024-03-15 06:38:32 +00:00
feng
fd64e71a3b perf: 优化user secret key 处理逻辑 2024-03-15 11:46:44 +08:00
feng626
7b990a264f Merge pull request #3782 from jumpserver/pr@dev@translate
perf: translate
2024-03-15 10:34:20 +08:00
feng
7687917aae perf: translate 2024-03-15 10:33:13 +08:00
wangruidong
02b71619de perf: 支持终断批量快捷命令执行的任务 2024-03-14 11:18:04 +08:00
Bai
da90b23f99 perf: 优化我的资产页面不能编辑 Labels 的问题 2024-03-14 11:08:58 +08:00
wangruidong
b8090abcdc perf: 添加oracle默认数据库提示 2024-03-14 11:08:31 +08:00
feng626
45b07dd628 Merge pull request #3777 from jumpserver/pr@dev@customize_footer
feat: 自定义footer
2024-03-12 14:34:44 +08:00
feng
ad7a6a5579 feat: 自定义footer 2024-03-12 14:22:28 +08:00
wangruidong
67c8521d5c fix: 自动任务跳转执行列表显示结果不准确的问题 2024-03-12 11:18:33 +08:00
wangruidong
18fc63b8c0 feat: 支持一键导出批量命令执行日志 2024-03-12 11:01:27 +08:00
w940853815
64d2854e49 Revert "fix: 刷新页面搜索条件丢失的问题"
This reverts commit 99f83a6bc4.
2024-03-11 18:20:24 +08:00
wangruidong
99f83a6bc4 fix: 刷新页面搜索条件丢失的问题 2024-03-07 14:22:20 +08:00
feng626
ca9f90624e Merge pull request #3772 from jumpserver/pr@dev@detail_formatter
perf: 支持点击“转到”后进行颜色标识
2024-03-06 19:11:00 +08:00
feng
05edf98514 perf: 支持点击“转到”后进行颜色标识 2024-03-06 19:10:04 +08:00
Bai
45b8c622bc perf: 优化界面设置页面主题Logo预览的背景颜色 2024-03-06 17:53:01 +08:00
feng626
faf1fb60b7 Merge pull request #3769 from jumpserver/pr@dev@account_gather
feat: 账号收集任务可单独添加资产
2024-03-05 15:00:27 +08:00
feng
f28825ba74 feat: 账号收集任务可单独添加资产 2024-03-05 14:59:02 +08:00
wangruidong
4672abae35 fix: 刷新页面根据搜索条件过滤出对应的资源 2024-03-04 19:17:05 +08:00
wangruidong
3626fd024d fix: 刷新页面根据搜索条件过滤出对应的资源 2024-03-04 19:13:18 +08:00
feng626
79ec0610a8 Merge pull request #3766 from jumpserver/revert-3764-pr@dev@fix_url_search
Revert "fix: 刷新页面根据搜索条件过滤出对应的资源"
2024-03-04 17:54:35 +08:00
Bryan
eee5889d51 Revert "fix: 刷新页面根据搜索条件过滤出对应的资源" 2024-03-04 17:53:46 +08:00
feng626
038ec49d5e Merge pull request #3764 from jumpserver/pr@dev@fix_url_search
fix: 刷新页面根据搜索条件过滤出对应的资源
2024-03-04 17:41:49 +08:00
wangruidong
669ffc73fd fix: 刷新页面根据搜索条件过滤出对应的资源 2024-03-04 14:24:39 +08:00
feng626
278562e855 Merge pull request #3763 from jumpserver/pr@dev@account_push
perf: 社区版账号推送,隐藏掉定时推送按钮
2024-03-01 16:57:37 +08:00
feng
fafa088a5e perf: 社区版账号推送,隐藏掉定时推送按钮 2024-03-01 16:51:06 +08:00
Bryan
ba36d72602 Merge pull request #3761 from jumpserver/master
v3.10.4 (branch-v3.10)
2024-02-29 16:26:13 +08:00
Bryan
4bfbbba4c5 Merge pull request #3760 from jumpserver/dev
v3.10.4
2024-02-29 16:15:33 +08:00
wangruidong
1ff65a2293 fix: sftp会话详情禁用监控按钮 2024-02-29 14:33:39 +08:00
Bai
1c69c61432 fix: 修复文件传输权限位 2024-02-29 14:13:42 +08:00
wangruidong
046870e366 fix: 隐藏停止任务按钮 2024-02-28 20:51:59 +08:00
feng626
b44af12f3b Merge pull request #3756 from jumpserver/pr@dev@push_params
fix: 平台中的改密参数和推送参数没了
2024-02-28 18:49:47 +08:00
feng
d9a9c7e229 fix: 平台中的改密参数和推送参数没了 2024-02-28 18:48:06 +08:00
wangruidong
55dfa5889b perf: 终断任务按钮状态变化优化 2024-02-28 10:00:13 +08:00
wangruidong
b35b5bd774 fix: 远程应用账号列表排版问题 2024-02-28 09:59:52 +08:00
feng626
1ee9e5df78 Merge pull request #3751 from jumpserver/pr@dev@push_account
fix: 【资产列表】资产详情中推送账号,推送参数未获取平台的参数
2024-02-27 16:56:50 +08:00
feng
2fe6cb37e6 fix: 【资产列表】资产详情中推送账号,推送参数未获取平台的参数 2024-02-27 16:55:48 +08:00
feng626
da570e21ee Merge pull request #3747 from jumpserver/pr@dev@luna
fix: 跳转到 luna 组织不对
2024-02-27 10:23:55 +08:00
wangruidong
16adcd299a perf: 平台参数联动逻辑优化 2024-02-26 19:49:26 +08:00
wangruidong
df8a464c36 fix: Web资产的选择器未根据平台参数联动 2024-02-26 16:40:15 +05:00
wangruidong
52e121cfdb perf: 禁用redis/clickhouse测试可连接性按钮 2024-02-26 15:53:20 +05:00
feng626
f014fc6426 Merge pull request #3748 from jumpserver/pr@dev@user_group
perf: Default组织下,标签关联用户资源,去掉组件用户
2024-02-26 17:21:00 +08:00
feng
88a6c2bb2b perf: Default组织下,标签关联用户资源,去掉组件用户 2024-02-26 17:08:05 +08:00
feng
8fa31fe0c2 fix: 跳转到 luna 组织不对 2024-02-26 16:24:50 +08:00
wangruidong
8f246c18e1 perf: 作业日志添加任务类型 2024-02-26 13:43:25 +08:00
wangruidong
9d999a7119 fix: LDAP用户导入会超时 2024-02-22 11:37:58 +08:00
wangruidong
dc8f237fec fix: 类别、类型排序无效 2024-02-21 14:21:38 +08:00
jiangweidong
41d0615ab5 feat: 支持工单链接直接免密审批 2024-02-21 11:39:57 +08:00
feng626
fadc3e7dd0 Merge pull request #3736 from jumpserver/pr@dev@account
perf: 账号收集添加资产名称模糊搜索
2024-02-20 18:41:58 +08:00
feng
152f56b496 perf: 账号收集添加资产名称模糊搜索 2024-02-20 18:39:48 +08:00
wangruidong
ed4f8dea90 perf: 终断批量快捷命令执行的任务 2024-02-20 15:13:11 +08:00
feng626
dced020a20 Merge pull request #3734 from jumpserver/pr@dev@perm_user
perf: 授权用户列表显示角色
2024-02-19 14:48:46 +08:00
feng
bf3c87575c perf: 授权用户列表显示角色 2024-02-19 14:43:01 +08:00
Eric
dadb54090c perf: 会话活动日志 2024-02-06 18:32:53 +08:00
ibuler
3f683b012c perf: 修改 ai chat 的位置 2024-02-06 17:59:13 +08:00
wangruidong
ecb1e91136 fix: Web资产选择器需跟平台的参数适配 2024-02-05 18:17:33 +08:00
wangruidong
454947f08b perf: 支持改密日志记录保留天数 2024-02-05 18:07:04 +08:00
feng
3ff6c6fe2f fix: 导出下载更新模版用不同的action 去对应后台serializer 2024-02-05 17:51:56 +08:00
Bai
527cc4d727 fix: 修复用户登录403的问题(DEFAULT组织由后端进行设置) 2024-02-05 16:52:31 +08:00
wangruidong
3b4201d2bf perf: 修改翻译 2024-02-05 15:54:42 +08:00
wangruidong
ba109da324 perf: 禁止用户自身更新自己的某些属性 2024-02-04 18:01:21 +08:00
jiangweidong
2a92c7657c perf: 支持账号批量更新功能 (#3717) 2024-02-04 17:50:22 +08:00
feng626
beb8ace5bd Merge pull request #3723 from jumpserver/pr@dev@asset_select
perf: 改密推送 选择资产支持标签搜索
2024-02-04 17:29:13 +08:00
feng
5e1225524c perf: 改密推送 选择资产支持标签搜索 2024-02-04 17:27:46 +08:00
wangruidong
931042eb2f perf: 国际电话区号选项从api返回 2024-02-04 14:52:50 +08:00
feng626
383577bb18 Merge pull request #3721 from jumpserver/pr@dev@ssh_key
perf: 【模版账号】创建/更新 模版账号表单中,希望增加上传密钥按钮
2024-02-02 18:44:59 +08:00
feng
f9e94386de perf: 【模版账号】创建/更新 模版账号表单中,希望增加上传密钥按钮 2024-02-02 18:41:11 +08:00
feng
5879eed926 perf: 优化用户session 会话过期 2024-02-02 17:54:46 +08:00
wangruidong
e1e54bf7a3 fix: 控制台,审计台仪表盘图表显示不对 2024-01-31 13:55:05 +08:00
ibuler
9e0c43589d perf: 优化首页日期 tab 按钮的颜色 2024-01-31 10:00:11 +08:00
wangruidong
ca3b0cfce5 perf: crontab修改提示翻译 2024-01-30 17:48:25 +08:00
jiangweidong
245b3f4ad2 perf: 资源详情页面标题长度超过一行则省略表示 2024-01-29 16:52:12 +08:00
ibuler
ea575e0515 perf: 优化数量显示,异步获取 hover 的内容 2024-01-29 16:50:42 +08:00
“huailei000”
ff63d2ca39 perf: 优化快捷命令输出框跟随浏览器高度 2024-01-29 16:49:38 +08:00
feng626
045af27999 Merge pull request #3711 from jumpserver/pr@dev@account_template
feat: 账号模版可导入导出
2024-01-29 16:42:45 +08:00
feng626
257365932c Merge pull request #3710 from jumpserver/pr@dev@account_bulk_test
feat: 批量测试账号可连接性
2024-01-29 16:42:18 +08:00
feng
aca4e4077f feat: 账号模版可导入导出 2024-01-29 16:19:19 +08:00
feng
ce80d36b8b feat: 批量测试账号可连接性 2024-01-29 14:40:49 +08:00
wangruidong
7dd5256303 perf: 用户详情 - 授权资产减少默认字段 2024-01-25 17:19:09 +08:00
wangruidong
fcf1093b4c perf: 安全模式返回授权的资产 2024-01-25 17:08:15 +08:00
feng626
5f9e9afffb Merge pull request #3709 from jumpserver/pr@dev@translate
perf: 翻译
2024-01-25 14:36:18 +08:00
feng
1bba9980c2 perf: 翻译 2024-01-25 14:35:13 +08:00
halo
6cbcee7656 perf: 指定账号enter异常刷新整个页面 2024-01-22 06:38:19 +00:00
halo
2084c50f95 perf: 资产授权弹窗选择时点击遮罩退出问题 2024-01-22 06:37:53 +00:00
“huailei000”
20d98bf09e perf: 优化更多操作高度;优化快捷命令右侧图标;优化账号管理列表-定期执行在中、英文状态下的宽度 2024-01-19 10:32:38 +00:00
“huailei000”
05c2f1f859 perf: 快捷命令toolbar增加折叠功能 2024-01-19 10:31:36 +00:00
“huailei000”
1e9107ec4a perf: 兼容luna显示智能问答 2024-01-19 09:23:19 +00:00
wangruidong
96a3f0a334 fix: 网关列表资产数量点击详情中没有资产 2024-01-18 08:11:46 +00:00
“huailei000”
6938299940 perf: 会话详情添加文件传输;操作日志默认显示动作、资源类型 2024-01-18 08:10:58 +00:00
wangruidong
0d1eb82fca perf: The cron interval execution must be greater than 10 minutes 2024-01-18 08:06:14 +00:00
Bryan
ea038ce43a Merge pull request #3697 from jumpserver/master
v3.10.2
2024-01-17 13:34:12 +00:00
Bryan
e16b19666c Merge pull request #3696 from jumpserver/dev
v3.10.2
2024-01-17 13:33:22 +00:00
feng626
ddf268e8ec Merge pull request #3695 from jumpserver/revert-3692-pr@dev@login_expire
Revert "perf: 登录过期自动退出"
2024-01-17 21:11:10 +08:00
老广
ae6fb878da Revert "perf: 登录过期自动退出" 2024-01-17 21:07:33 +08:00
Bryan
c7f5409eb6 Merge pull request #3694 from jumpserver/master
v3.10.2
2024-01-17 07:35:33 -04:00
Bryan
fdbd7d2222 Merge pull request #3693 from jumpserver/dev
v3.10.2
2024-01-17 07:24:50 -04:00
feng626
d7099c118b Merge pull request #3692 from jumpserver/pr@dev@login_expire
perf: 登录过期自动退出
2024-01-17 15:58:16 +08:00
feng
1b73591366 perf: 登录过期自动退出 2024-01-17 15:47:21 +08:00
ibuler
6f3f66df73 perf: auto decide create in new page or current page in asset or permission 2024-01-15 19:37:47 +08:00
ibuler
598020a89b perf: revert asset create with labels 2024-01-15 19:36:48 +08:00
“huailei000”
88486e2b00 perf: 优化时间区间选择组件选择时间不准确问题 2024-01-14 22:57:36 -08:00
“huailei000”
faf8521c91 perf: 优化标签列表-资产数量弹窗中的搜索条件不显示在url中 2024-01-14 22:29:42 -08:00
feng
ccd74fb76f fix: m2m 表单渲染错乱 2024-01-14 22:14:48 -08:00
ibuler
d76c6fdbd8 perf: 优化 tag search 避免多次请求 2024-01-14 18:29:12 -08:00
wangruidong
af6a55d3f4 feat: 同步ldap用户消息通知 2024-01-12 12:02:42 +05:00
“huailei000”
c052961efe perf: 优化仪表盘翻译 2024-01-11 14:43:24 +08:00
“huailei000”
57d339f513 perf: 优化批量操作图标大小 2024-01-11 14:43:01 +08:00
feng626
097771175d Merge pull request #3678 from jumpserver/pr@dev@dashboard_translate
perf: 前端dashboard 翻译
2024-01-09 17:49:43 +08:00
feng
0922557abc perf: 前端dashboard 翻译 2024-01-09 17:40:37 +08:00
feng626
6842da1960 Merge pull request #3676 from jumpserver/pr@dev@perm_create
perf: 【资产授权】选择资产弹窗左侧树中,去掉搜索、刷新按钮
2024-01-08 17:26:27 +08:00
feng
eb839b4113 perf: 【资产授权】选择资产弹窗左侧树中,去掉搜索、刷新按钮 2024-01-08 17:25:34 +08:00
“huailei000”
7e3e8fbf2f perf: 替换批量更新图标 2024-01-08 16:04:37 +08:00
jiangweidong
3800151763 perf: 邮箱支持exchange协议 2024-01-08 12:34:50 +05:00
ibuler
0121505f28 perf: 优化页面显示 2024-01-08 14:49:12 +08:00
ibuler
b9a6f5d3ac perf: 修复标签导入和搜索的问题 2024-01-03 17:07:15 +08:00
feng626
179b568b16 Merge pull request #3672 from jumpserver/pr@dev@history_account
feat: 历史账号定期删除 可设置保留数量
2024-01-03 11:04:22 +08:00
feng
c563697efd feat: 历史账号定期删除 可设置保留数量 2024-01-02 19:10:54 +08:00
fit2bot
fa9281aa92 perf: 优化节点树 (#3670)
Co-authored-by: ibuler <ibuler@qq.com>
2024-01-02 16:18:54 +08:00
Bryan
ddbaeeafea Merge pull request #3668 from jumpserver/master
v3.10.1
2023-12-29 11:34:04 +05:00
Bryan
efb0e9dacb Merge pull request #3665 from jumpserver/dev
v3.10.1
2023-12-29 11:14:54 +05:00
feng
cc8d94f666 fix: Default组织下,标签关联用户资源,去掉组件用户 2023-12-29 11:14:26 +05:00
“huailei000”
1416405644 perf: 优化批量更新资产不能使用问题 2023-12-29 11:10:04 +05:00
wangruidong
d29b3effbc fix: 更新用户组权限问题 2023-12-29 07:41:28 +05:00
wangruidong
bd9456ba2d fix: 创建用户失败 2023-12-28 15:35:22 +05:00
wangruidong
0c9d5d9b6b fix: 用户角色修改后更新页面与用户列表页显示不一致 2023-12-28 15:35:22 +05:00
Bai
b19ddd6799 fix: 修复组件中没有暴露 SQLServer 端口的问题 2023-12-28 14:52:41 +05:00
wangruidong
ea48b6ebf3 perf: 修改文件上传超时时间 2023-12-27 18:57:17 +08:00
“huailei000”
fba2f77874 perf: 优化智能问答返回显示系统消息 2023-12-27 17:11:20 +08:00
wangruidong
3c900ce387 perf: 账号删除添加提示确认框 2023-12-27 14:07:15 +05:00
“huailei000”
7df11b907f perf: 优化资产详情页面布局 2023-12-27 15:27:43 +08:00
wangruidong
cdd51a9c16 perf: 统计任务执行结果 2023-12-26 15:45:08 +08:00
wangruidong
51a4c013d3 perf: 修改文件上传超时时间 2023-12-26 10:48:36 +08:00
feng
111aafb4bb fix: 【用户登录会话失效问题】SESSION_COOKIE_AGE 配置不生效的问题 2023-12-25 13:24:02 +05:00
“huailei000”
81f0b13730 perf: 优化网关克隆平台显示不正确问题 2023-12-25 15:10:10 +08:00
“huailei000”
a36a9e7645 perf: 智能问答禁止密码自动填充 2023-12-22 13:23:17 +05:00
huailei
f6f8301ad5 Revert "perf: 账号收集翻译"
This reverts commit 9a63ae63d4.
2023-12-22 15:25:31 +08:00
Eric
cfe6db6ec5 perf: 虚拟应用增加许可证验证 2023-12-22 12:24:08 +05:00
“huailei000”
ccb221559a perf: 账号收集翻译 2023-12-22 11:33:43 +08:00
“huailei000”
9a63ae63d4 perf: 账号收集翻译 2023-12-22 11:31:45 +08:00
Bryan
1e007ccda3 Merge pull request #3642 from jumpserver/dev
v3.10
2023-12-21 15:15:52 +05:00
“huailei000”
ace8501648 perf: 实体组织下批量更新用户可以选择用户组 2023-12-21 17:29:38 +08:00
“huailei000”
ff627bb0db perf: 优化chat滚动条宽度 2023-12-21 16:19:59 +08:00
ibuler
dba8d84f2d perf: 优化导入 dialog 大小 2023-12-21 15:38:34 +08:00
wangruidong
e24bfff6ab fix: 同名文件可以删除任意一个 2023-12-21 15:38:15 +08:00
wangruidong
cdcc4226db perf: 界面设置提交后不刷新页面 2023-12-21 15:37:26 +08:00
“huailei000”
09b52738ca perf: 优化站内信布局;显示站内信关闭按钮 2023-12-21 11:02:42 +08:00
“huailei000”
e324b16e1b perf: 优化标签管理编辑按钮位置 2023-12-21 10:26:20 +08:00
fit2bot
1b52e4cb93 perf: 优化 label search (#3634)
Co-authored-by: ibuler <ibuler@qq.com>
2023-12-20 20:48:00 +08:00
“huailei000”
6ac31b05e5 perf: window资产密文类型不是密码时不能设置推送参数 2023-12-20 19:37:57 +08:00
wangruidong
6c24c9a2d9 fix: 账号备份开启拆分两部分后再关闭还是会拆成两份发送邮件 2023-12-20 16:25:17 +05:00
fit2bot
b79ef4f7a8 perf: 更新白名单后返回列表页 (#3630)
Co-authored-by: wangruidong <940853815@qq.com>
2023-12-20 19:18:04 +08:00
“huailei000”
d584f83f59 perf: 用户列表全局组织下可以批量更新用户,不能更新用户组 2023-12-20 19:00:00 +08:00
feng626
22ed0ec0ca Merge pull request #3629 from jumpserver/pr@dev@user
perf: 更新创建用户 权限位关联,用户角色 select2
2023-12-20 18:29:09 +08:00
feng
436e9e59f1 perf: 更新创建用户 权限位关联,用户角色 select2 2023-12-20 18:27:44 +08:00
wangruidong
ba63d52275 fix: 添加资产id查询条件 2023-12-20 15:14:45 +05:00
“huailei000”
aa48e24881 perf: 全局组织下禁止批量更新 2023-12-20 17:57:29 +08:00
“huailei000”
f59b33a27f perf: 优化标签组件内容为空时面板的位置 2023-12-20 17:38:57 +08:00
ibuler
9f87708b96 perf: 修改拼写错误 2023-12-20 17:02:57 +08:00
Eric
78819e9a04 perf: 页面配置是否启用 Vitual App 2023-12-20 13:19:49 +05:00
“huailei000”
8e981d52eb perf: 优化标签管理编辑 2023-12-20 15:52:33 +08:00
wangruidong
9f4c798ddb perf: 修改文件上传超时时间 2023-12-19 16:18:36 +05:00
ibuler
7eac62635b perf: 支持点击 label 搜索 2023-12-19 17:00:50 +08:00
ibuler
3739d710f8 perf: 标签支持多个搜索 2023-12-19 15:26:10 +08:00
“huailei000”
aae5aa9d7f perf: 标签列表关联成功资产后刷新列表 2023-12-19 14:54:19 +08:00
“huailei000”
88be5d5fe8 perf: 优化资产选择节点组件取消不能关闭弹窗问题 2023-12-19 13:57:37 +08:00
fit2bot
6ac54c9865 perf: 优化 labels list 显示 (#3617)
* perf: 修改 label list 显示

* perf: 优化 labels list 显示

---------

Co-authored-by: ibuler <ibuler@qq.com>
2023-12-18 22:38:35 +08:00
ibuler
5cefbbfb51 perf: 修改 labels 关联 2023-12-18 18:06:18 +08:00
ibuler
f10e31bd60 perf: 修复标签搜索 2023-12-15 18:32:28 +08:00
wangruidong
e8e8b5bfca perf: 文件传输提示优化 2023-12-15 18:12:19 +08:00
“huailei000”
19adbca7b3 perf:优化资产授权详情授权用户、授权资产点击多选框会全部选中问题 2023-12-15 15:11:05 +08:00
feng626
ba28d5619a Merge pull request #3612 from jumpserver/pr@dev@acl_rules
perf: acl 自定义规则添加首次登录选项
2023-12-15 14:16:26 +08:00
feng
68363a4c52 perf: acl 自定义规则添加首次登录选项 2023-12-15 14:13:43 +08:00
“huailei000”
f947fa8d36 perf: 优化新建聊天不重新创建socket 2023-12-15 11:03:45 +08:00
ibuler
c1d0994781 perf: 优化 chat css 2023-12-14 19:53:17 +08:00
fit2bot
fde576fe8b perf: 上传目标目录指定在/tmp下 (#3608)
Co-authored-by: wangruidong <940853815@qq.com>
2023-12-14 19:45:27 +08:00
“huailei000”
27864be41d perf: 优化chat初始化socket 2023-12-14 19:32:31 +08:00
ibuler
9d461be4a0 perf: 优化聊天样式 2023-12-14 19:12:03 +08:00
“huailei000”
bcbfe114d8 perf: 优化点击新聊天提示词没有重置问题 2023-12-14 17:37:13 +08:00
ibuler
af8448adeb merge: with dev 2023-12-14 16:59:46 +08:00
fit2bot
9b7a360ea1 perf: chat增加提示词 (#3603)
Co-authored-by: “huailei000” <2280131253@qq.com>
Co-authored-by: huailei <31801270+huailei000@users.noreply.github.com>
2023-12-14 16:37:48 +08:00
ibuler
3bf7eed52e perf: 修复资产详情中 label 的问题 2023-12-14 16:36:59 +08:00
ibuler
c1d80469db perf: 修改 prompt 2023-12-14 14:07:38 +08:00
jiangweidong
e6a1039387 feat: 云同步支持设置策略关系、支持不匹配不符合策略的资产 (#3592) 2023-12-14 11:31:23 +08:00
wangruidong
ad7b2c4e8f perf: 上传大小限制从接口获取 2023-12-14 11:27:56 +08:00
fit2bot
d51a787598 perf: 使用slot处理文件列表 (#3599)
* perf: 使用slot处理文件列表

* perf: 大小文件超出限制不上传

---------

Co-authored-by: wangruidong <940853815@qq.com>
2023-12-14 10:34:06 +08:00
ibuler
2ac9183047 perf: 修改 icon 颜色 2023-12-14 10:30:42 +08:00
ibuler
3b9f0b56fb perf: 优化右侧聊天 2023-12-13 18:08:42 +08:00
“huailei000”
d900a8d5a3 perf: 已锁定的ip按钮增加个数显示 2023-12-13 17:16:16 +08:00
fit2bot
6daa16b3ca perf: add strip-ansi (#3596)
* perf: 修改 requrements

* perf: add strip-ansi

---------

Co-authored-by: ibuler <ibuler@qq.com>
2023-12-13 16:38:45 +08:00
“huailei000”
28891234db perf: 优化socket地址 2023-12-13 16:34:24 +08:00
ibuler
1d7bdcb512 perf: 修改 requrements 2023-12-13 16:31:41 +08:00
“huailei000”
0964031922 perf: 优化chat 停止按钮位置 2023-12-13 16:14:15 +08:00
ibuler
6568f47760 perf: 优化文件上传的样式 2023-12-13 11:24:04 +08:00
“huailei000”
448e89c733 perf: update yarn.lock 2023-12-13 11:20:11 +08:00
“huailei000”
68dccaa93c perf: 优化抽屉按钮效果 2023-12-13 10:54:50 +08:00
“huailei000”
b0cefb05e0 perf: 优化chat提示 2023-12-12 19:48:35 +08:00
wangruidong
d6bf4c86df perf: 显示上传文件大小 2023-12-12 19:07:02 +08:00
feng626
993ded5ca8 Merge pull request #3586 from jumpserver/pr@dev@perf_chat
perf: chat
2023-12-12 18:52:19 +08:00
huailei
951df9e63a Merge branch 'dev' into pr@dev@perf_chat 2023-12-12 18:51:43 +08:00
“huailei000”
4b98806b3e perf: chat 2023-12-12 18:50:19 +08:00
ibuler
536520c78c perf: 优化英语翻译 2023-12-12 18:43:51 +08:00
ibuler
63650912b8 perf: placeholder 字体大小 2023-12-12 18:43:31 +08:00
feng626
b65069adeb Merge pull request #3583 from jumpserver/pr@dev@translate
perf: 翻译
2023-12-12 16:35:00 +08:00
feng
7bd3179488 perf: 翻译 2023-12-12 16:33:16 +08:00
feng
bf6ecca1fa perf: 翻译 2023-12-12 16:32:54 +08:00
ibuler
69449740df perf: 修改一下大小 2023-12-12 14:16:35 +08:00
feng626
591f3ae39a Merge pull request #3579 from jumpserver/pr@dev@system_task
perf: 【优化系统任务】支持显示 执行周期、下次开始时间 字段
2023-12-12 14:16:12 +08:00
ibuler
463d2b16bf perf: chatai icon 2023-12-12 11:13:15 +08:00
feng
cc47d93988 perf: 【优化系统任务】支持显示 执行周期、下次开始时间 字段 2023-12-12 11:06:53 +08:00
fit2bot
19e42c48e4 feat: 支持批量发送文件 (#3574)
* feat: 支持批量发送文件

* feat: 支持批量发送文件

* perf: 同名文件处理

---------

Co-authored-by: wangruidong <940853815@qq.com>
2023-12-12 10:40:41 +08:00
“huailei000”
6b37c71b6d feat: 添加chat聊天 2023-12-11 18:24:24 +08:00
feng
3af6b1d1fe feat: 系统设置配置gpt 2023-12-11 18:24:24 +08:00
fit2bot
2445ecb6e5 perf: 优化 select2 成 transfer select2 (#3576)
* feat: 添加 tranSelect

* perf: 优化 select2 成 transfer select2

---------

Co-authored-by: ibuler <ibuler@qq.com>
2023-12-11 16:14:06 +08:00
feng626
a5ae4110da Merge pull request #3578 from jumpserver/pr@dev@applet
perf: 远程应用上传过程中,如果应用不存在,则创建该应用;如果已存在,则进行应用更新。
2023-12-11 14:29:11 +08:00
feng
dc4378b637 perf: 远程应用上传过程中,如果应用不存在,则创建该应用;如果已存在,则进行应用更新。 2023-12-11 14:26:43 +08:00
Eric
91f2de7797 perf: 添加 panda 监控显示 2023-12-11 14:14:39 +08:00
Eric
29b2829199 perf: 完善 app provider 详情页 2023-12-08 18:48:15 +08:00
feng626
4e23a34af9 Merge pull request #3572 from jumpserver/pr@dev@delete_account_automation
feat: 同步删除远程机器账号
2023-12-08 14:14:28 +08:00
feng
2bd1aaa03e feat: 同步删除远程机器账号 2023-12-07 21:07:29 +08:00
halo
96e23ddb52 fix: 修复字段翻译问题 2023-12-07 14:10:37 +08:00
fit2bot
351e250688 feat: support virtual app (#3558)
* feat: support virtual app

* perf: virtual host 详情

* perf: 调整 virtual app 卡片展示

* perf: 调整 route

* perf: 增加翻译

* perf: 优化更换名称

---------

Co-authored-by: Eric <xplzv@126.com>
2023-12-06 10:38:04 +08:00
liufei20151013
e8ebd1aa64 feat: 云同步支持 ZStack 2023-12-05 17:35:41 +08:00
jiangweidong
6adca44180 perf: 用户列表可展示更多api返回的字段信息 2023-12-05 17:28:33 +08:00
ibuler
f3699069c5 perf: 优化创建 label 2023-12-05 15:15:55 +08:00
fit2bot
2f298a32c3 perf: 修改标签 (#3516)
* perf: 支持全局的 labels

* perf: 修改标签

* perf: 修改 labes

* stash

* pref: stash it

* perf: stash

* perf: 优化 labels

* perf: 优化 tabs

* perf: 优化 tag

* perf: 基本完成

* perf: 优化 labels 创建搜索

* perf: 优化 labels

* perf: 优化完成标签功能

* perf: 修改 label name

---------

Co-authored-by: ibuler <ibuler@qq.com>
2023-12-05 10:30:56 +08:00
fit2bot
3ba81a43cb feat: 资产详情页面添加历史执行命令列表页面 (#3568)
* feat: 资产详情页面添加历史执行命令列表页面

---------

Co-authored-by: wangruidong <940853815@qq.com>
2023-12-04 15:20:57 +08:00
fit2bot
813f17b52f perf: 优化工单处理提示消息页面 (#3567)
Co-authored-by: wangruidong <940853815@qq.com>
2023-11-30 11:40:04 +08:00
jiangweidong
0fc292b1ad perf: 支持slack通知和认证 (#3556) 2023-11-29 17:50:09 +08:00
fit2bot
f3f5bbe114 feat: 用户详情展示所有会话 (#3563)
Co-authored-by: wangruidong <940853815@qq.com>
2023-11-28 19:07:08 +08:00
feng626
8c732f00e1 Merge pull request #3566 from jumpserver/pr@dev@random
perf: 随机密码生成规则添加可排除字符选项
2023-11-28 14:48:41 +08:00
feng
f91b8c75df perf: 随机密码生成规则添加可排除字符选项 2023-11-28 14:45:58 +08:00
jiangweidong
e86b88bf05 feat: 支持深信服云平台和阿里云专有云 2023-11-27 11:12:14 +08:00
“huailei000”
bf3a2d748d perf: 限制上传logo图片的格式;优化打开文件速度 2023-11-24 18:59:46 +08:00
wangruidong
8a4b81e2e5 feat: 支持备案配置 2023-11-24 13:56:28 +08:00
wangruidong
34406ec32d fix: SFTP对象存储,禁用设为默认存储 2023-11-24 10:32:56 +08:00
feng626
3269a2a3ff Merge pull request #3554 from jumpserver/pr@dev@perf_ldap_user_websocket
perf: ldap接口请求换成websocket连接
2023-11-23 15:05:32 +08:00
feng626
5d50844753 Merge pull request #3555 from jumpserver/pr@dev@add_all_member
perf: 给用户组添加全部成员时,二次确认下
2023-11-22 19:06:04 +08:00
feng
97cc73a4fa perf: 给用户组添加全部成员时,二次确认下 2023-11-22 19:04:56 +08:00
wangruidong
574dae6236 perf: ldap接口请求换成websocket连接 2023-11-22 16:40:10 +08:00
“huailei000”
84c413f51a perf: 优化日志审计默认筛选截止时间多一天 2023-11-22 15:03:56 +08:00
“huailei000”
87a1cce4ca perf: 优化table列表溢出提示没有复制成功问题 2023-11-22 11:01:46 +08:00
吴小白
be89b5d6f2 Merge pull request #3547 from jumpserver/pr@dev@fix_version
fix: 修正 actions version 获取错误
2023-11-17 09:42:52 +08:00
吴小白
200c727426 fix: 修正 actions version 获取错误 2023-11-17 08:20:27 +08:00
老广
d1d0b06b53 Merge pull request #3546 from jumpserver/dev
v3.9.0
2023-11-16 18:25:10 +08:00
wangruidong
d563f66ba1 perf: 修改翻译 2023-11-16 16:21:58 +08:00
feng626
4592eeddd3 Merge pull request #3544 from jumpserver/pr@dev@user_session
perf: 在线会话添加活跃状态过滤
2023-11-16 14:41:50 +08:00
feng
d73a07dff2 perf: 在线会话添加活跃状态过滤 2023-11-16 14:40:16 +08:00
“huailei000”
e8857bf150 perf: ldap导入用户不显示操作栏 2023-11-16 10:48:15 +08:00
wangruidong
0abe33053f perf: 修改翻译 2023-11-15 19:33:51 +08:00
“huailei000”
dc9161c6a7 perf:优化el-data-table组件分页多选记录不准确问题 2023-11-15 19:03:56 +08:00
老广
d588be696b Merge pull request #3540 from jumpserver/pr@dev@perf_winrm_private
perf: 修改 winrm 用户不能连接
2023-11-15 15:40:03 +08:00
ibuler
4b2c806f28 perf: 修改 winrm 用户不能连接 2023-11-15 15:39:15 +08:00
老广
e28b026ead Merge pull request #3539 from jumpserver/pr@dev@fix_select_options_error
fix: 对象存储下拉无法自动加载
2023-11-15 14:58:55 +08:00
wangruidong
c7414e0199 fix: 对象存储下拉无法自动加载 2023-11-15 14:51:38 +08:00
“huailei000”
5a478ebaba perf: 优化用户详情字段不显示问题 2023-11-15 12:17:40 +08:00
老广
08577f0ae5 Merge pull request #3536 from jumpserver/pr@dev@perf_ticket_item_error
perf: 优化工单详情页面渲染卡顿问题
2023-11-15 11:43:16 +08:00
“huailei000”
38527c6a41 perf: 优化工单详情页面渲染卡顿问题 2023-11-15 11:41:23 +08:00
“huailei000”
01c5b7c4a8 perf: 优化登录复核页面卡顿不显示问题 2023-11-15 10:07:01 +08:00
wangruidong
b0b3e0d1c9 fix: 更新组件批量更新去掉sftp选项 2023-11-14 19:25:44 +08:00
jiangweidong
5bc273d057 fix: 云同步任务详情界面删除无用的按钮 2023-11-14 19:23:27 +08:00
wangruidong
e128576763 fix: 作业日志筛选用户出错 2023-11-14 19:22:58 +08:00
“huailei000”
58f4dca599 perf: 优化 TagInput 组件失去焦点后没有自动填充值问题 2023-11-14 17:29:21 +08:00
老广
a6dd7f23c7 Merge pull request #3530 from jumpserver/pr@dev@fix_user_confirm_dialog
perf: 优化用户确认窗口
2023-11-14 14:23:22 +08:00
ibuler
a267d46464 perf: 优化用户确认窗口 2023-11-14 14:19:33 +08:00
“huailei000”
8c829ee498 perf: 优化markdown组件判断 2023-11-14 14:02:49 +08:00
ibuler
bcfa95de06 perf: 修改用户确认 2023-11-14 13:44:54 +08:00
“huailei000”
56745ae944 perf: 优化个人信息详情字段信息显示 2023-11-14 11:35:04 +08:00
wangruidong
5a664f8c2c fix: 账号备份详情页显示sftp服务器信息 2023-11-13 17:06:18 +08:00
ibuler
1fd29e13f8 perf: 修改 reload 2023-11-13 14:55:27 +08:00
ibuler
af863dae75 perf: 修改 table reload 2023-11-13 14:44:38 +08:00
“huailei000”
b1254d2b87 perf: 优化markdown组件样式 2023-11-10 18:25:58 +08:00
“huailei000”
d350cadd4c perf: 优化资产账号模版反复勾选同一个账号会出现无法选中的问题 2023-11-10 16:10:31 +08:00
“huailei000”
cfe9ebd747 perf: 优化创建账号-指定账号组件重复选择问题;优化创建工单-制定账号重复选择问题 2023-11-10 11:16:56 +08:00
“huailei000”
03177d4ce9 perf: 调整公告背景颜色 2023-11-09 19:22:38 +08:00
ibuler
a55b0da22b perf: 优化用户确认 2023-11-09 18:48:42 +08:00
feng626
1b0bdf25ae Merge pull request #3514 from jumpserver/pr@dev@online_session_active
perf: 在线用户根据websocket添加用户是否活跃状态
2023-11-09 18:15:08 +08:00
“huailei000”
c746cdc639 perf: 平台列表不显示更多操作 2023-11-09 18:08:29 +08:00
“huailei000”
3cf6b6f639 perf: 优化保存快捷命令时,命令不能为空 2023-11-09 18:08:02 +08:00
feng
a1bf8f9ab7 perf: 在线用户根据websocket添加用户是否活跃状态 2023-11-08 16:51:16 +08:00
Bryan
5fb70d2f24 Merge pull request #3450 from jumpserver/dev
v3.8.0
2023-10-19 03:33:53 -05:00
Bryan
b54a95430f Merge pull request #3404 from jumpserver/dev
v3.7.0
2023-09-21 17:04:42 +08:00
Bryan
4d8b4c45af Merge pull request #3355 from jumpserver/dev
v3.6.0
2023-08-17 14:00:33 +05:00
Bryan
a6d642df60 Merge pull request #3283 from jumpserver/dev
v3.5.0
2023-07-20 19:04:29 +08:00
Jiangjie.Bai
2e74f1522f Merge pull request #3222 from jumpserver/dev
v3.4.0
2023-06-15 14:51:36 +08:00
Jiangjie.Bai
fe615e0314 Merge pull request #3219 from jumpserver/dev
v3.4.0
2023-06-15 14:17:46 +08:00
Jiangjie.Bai
09f734e6fc Merge pull request #3135 from jumpserver/dev
v3.3.0
2023-05-18 19:18:11 +08:00
Jiangjie.Bai
3117046342 Merge pull request #3061 from jumpserver/dev
v3.2.0
2023-04-20 18:40:08 +08:00
Bai
b68aecb5cc fix: 批量更新资产平台help-text 2023-04-20 18:39:22 +08:00
Jiangjie.Bai
1c9b155d97 Merge pull request #3057 from jumpserver/dev
v3.2.0
2023-04-20 18:22:46 +08:00
Jiangjie.Bai
75b1be9864 Merge pull request #3019 from jumpserver/dev
v3.2.0 rc2
2023-04-14 19:01:37 +08:00
Jiangjie.Bai
615c3c1cf4 Merge pull request #3014 from jumpserver/dev
v3.2.0 rc1
2023-04-13 20:02:38 +08:00
Jiangjie.Bai
4d82231af4 Merge pull request #3012 from jumpserver/dev
v3.2.0 rc1
2023-04-13 19:22:38 +08:00
“huailei000”
c6cf6571b6 perf: ldap导入用户列表-组织下拉框设置最大宽度 2023-03-16 16:44:36 +08:00
Bai
8ea990d070 fix: 修复创建资产添加账号模版报错问题 2023-03-16 16:44:36 +08:00
“huailei000”
f4a32170d5 perf: message 2023-03-16 16:44:36 +08:00
ibuler
073508675e perf: 添加默认的信息 2023-03-16 16:44:36 +08:00
Jiangjie.Bai
1d6ca0a93a Merge pull request #2924 from jumpserver/dev
v3.1.0 rc4
2023-03-15 19:46:31 +08:00
Jiangjie.Bai
36aea652d6 Merge pull request #2788 from jumpserver/dev
v3.0.0
2023-02-23 20:16:41 +08:00
Jiangjie.Bai
1a42ce90ab Merge pull request #2760 from jumpserver/dev
v3.0.0-rc-latest
2023-02-22 22:21:54 +08:00
Jiangjie.Bai
31a401b55d Merge pull request #2463 from jumpserver/dev
v3.0.0-rc4
2023-01-31 18:55:34 +08:00
Jiangjie.Bai
582a84178d Merge pull request #2187 from jumpserver/dev
v2.28.0
2022-11-17 17:44:19 +08:00
Jiangjie.Bai
9b9f7c936c Merge pull request #2184 from jumpserver/dev
v2.28.0-rc5
2022-11-17 14:18:15 +08:00
Jiangjie.Bai
2a6100957f Merge pull request #2182 from jumpserver/dev
v2.28.0-rc4
2022-11-16 21:08:55 +08:00
Jiangjie.Bai
16606d6a27 Merge pull request #2176 from jumpserver/dev
v2.28.0-rc2
2022-11-14 10:01:05 +08:00
Jiangjie.Bai
0a612f50e6 Merge pull request #2164 from jumpserver/dev
v2.28.0-rc1
2022-11-10 17:45:47 +08:00
Jiangjie.Bai
fe36fa9390 Merge pull request #2117 from jumpserver/dev
v2.27.0-rc4
2022-10-18 21:02:10 +08:00
Jiangjie.Bai
ba109900ec Merge pull request #2113 from jumpserver/dev
v2.27.0-rc3
2022-10-18 11:20:57 +08:00
Jiangjie.Bai
ec7768267f Merge pull request #2105 from jumpserver/dev
v2.27.0-rc2
2022-10-14 11:01:32 +08:00
Jiangjie.Bai
cc58b374ab Merge pull request #2101 from jumpserver/dev
v2.27.0-rc1
2022-10-13 17:44:53 +08:00
Jiangjie.Bai
04ffbb8fd6 Merge pull request #2097 from jumpserver/dev
v2.27.0-rc1
2022-10-13 15:14:40 +08:00
Jiangjie.Bai
49880f6739 Merge pull request #2059 from jumpserver/dev
v2.26.0
2022-09-15 17:49:44 +08:00
Jiangjie.Bai
e6f98d58c4 Merge pull request #2057 from jumpserver/dev
v2.26.0-rc4
2022-09-15 16:18:03 +08:00
Jiangjie.Bai
fd1f16d43c Merge pull request #2050 from jumpserver/dev
v2.26.0-rc2
2022-09-13 17:41:39 +08:00
Jiangjie.Bai
968b2415b1 Merge pull request #2043 from jumpserver/dev
v2.26.0-rc1
2022-09-08 15:46:44 +08:00
Jiangjie.Bai
776090d6ba Merge pull request #2001 from jumpserver/dev
v2.25.0
2022-08-18 16:12:45 +08:00
Jiangjie.Bai
3a37952288 Merge pull request #1996 from jumpserver/dev
v2.25.0-rc4
2022-08-17 16:53:23 +08:00
Jiangjie.Bai
62b8fc0e3b Merge pull request #1994 from jumpserver/dev
v2.25.0-rc3
2022-08-16 19:08:23 +08:00
Jiangjie.Bai
b2028869cb Merge pull request #1986 from jumpserver/dev
v2.25.0-rc2
2022-08-12 18:06:56 +08:00
Jiangjie.Bai
5277a725f8 Merge pull request #1973 from jumpserver/dev
v2.25.0-rc1
2022-08-11 14:11:59 +08:00
Jiangjie.Bai
f137788c1a Merge pull request #1957 from jumpserver/dev
v2.24.0-rc5
2022-07-20 19:06:03 +08:00
Jiangjie.Bai
f7d17c8de7 Merge pull request #1954 from jumpserver/dev
v2.24.0-rc4
2022-07-19 16:18:13 +08:00
Jiangjie.Bai
feea70b0be Merge pull request #1944 from jumpserver/dev
v2.24.0-rc3
2022-07-18 12:05:42 +08:00
Jiangjie.Bai
04696ef3d6 Merge pull request #1940 from jumpserver/dev
v2.24.0-rc2
2022-07-15 18:07:37 +08:00
Jiangjie.Bai
1731f4f788 Merge pull request #1934 from jumpserver/dev
v2.24.0-rc1
2022-07-14 18:27:51 +08:00
Jiangjie.Bai
6f25d93909 Merge pull request #1931 from jumpserver/dev
v2.24.0-rc1
2022-07-14 17:51:58 +08:00
Jiangjie.Bai
46461ec324 Merge pull request #1925 from jumpserver/dev
v2.24.0-rc1
2022-07-14 15:12:15 +08:00
310 changed files with 16048 additions and 4072 deletions

View File

@@ -22,4 +22,5 @@ VUE_APP_LOGOUT_PATH = '/core/auth/logout/'
# Dev server for core proxy
VUE_APP_CORE_HOST = 'http://localhost:8080'
VUE_APP_CORE_WS = 'ws://localhost:8080'
VUE_APP_KAEL_HOST = 'http://localhost:8083'
VUE_APP_ENV = 'development'

View File

@@ -10,3 +10,4 @@ jobs:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}

View File

@@ -19,9 +19,7 @@ jobs:
id: get_version
run: |
TAG=$(basename ${GITHUB_REF})
VERSION=${TAG/v/}
echo "::set-output name=TAG::$TAG"
echo "::set-output name=VERSION::$VERSION"
- name: Create Release
id: create_release
uses: release-drafter/release-drafter@v5
@@ -43,10 +41,10 @@ jobs:
- name: Create Upload Assets
run: |
rm -rf build/*
mv lina lina-${{ steps.get_version.outputs.VERSION }}
tar -czf lina-${{ steps.get_version.outputs.VERSION }}.tar.gz lina-${{ steps.get_version.outputs.VERSION }}
echo $(md5sum lina-${{ steps.get_version.outputs.VERSION }}.tar.gz | awk '{print $1}') > build/lina-${{ steps.get_version.outputs.VERSION }}.tar.gz.md5
mv lina-${{ steps.get_version.outputs.VERSION }}.tar.gz build/
mv lina lina-${{ steps.get_version.outputs.TAG }}
tar -czf lina-${{ steps.get_version.outputs.TAG }}.tar.gz lina-${{ steps.get_version.outputs.TAG }}
echo $(md5sum lina-${{ steps.get_version.outputs.TAG }}.tar.gz | awk '{print $1}') > build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
mv lina-${{ steps.get_version.outputs.TAG }}.tar.gz build/
- name: Release Upload Assets
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
93ea7f034ae8393b11370557ff786f67062ed913

View File

@@ -20,26 +20,28 @@
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja"
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@traptitech/markdown-it-katex": "^3.6.0",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.21.1",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"cron-parser": "^4.0.0",
"crypto-js": "^4.1.1",
"css-color-function": "^1.3.3",
"decimal.js": "^10.4.3",
"deepmerge": "^4.2.2",
"echarts": "^4.7.0",
"echarts": "4.7.0",
"element-ui": "2.13.2",
"eslint-plugin-html": "^6.0.0",
"highlight.js": "^11.9.0",
"install": "^0.13.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"krry-transfer": "^1.7.3",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
@@ -54,6 +56,8 @@
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.4",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
@@ -79,7 +83,7 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.0.0",
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
@@ -93,6 +97,7 @@
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"deasync": "^0.1.29",
"element-theme-chalk": "^2.13.1",
"eslint": "^5.15.3",
"eslint-plugin-vue": "5.2.2",
@@ -109,6 +114,7 @@
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.13.2",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",

View File

@@ -27,6 +27,9 @@
if(pathname.indexOf('/ui') === -1) {
window.location.href = window.location.origin + '/ui/#' + pathname
}
if (pathname.startsWith('/ui/#/chat')) {
window.location.href = window.location.origin + pathname
}
}
</script>
<div id="app"></div>

View File

@@ -424,7 +424,7 @@ td .el-button.el-button--mini {
}
.el-dialog .el-dialog__body {
max-height: 90vh;
max-height: 80vh;
overflow: auto;
padding: 30px;
}

View File

@@ -42,8 +42,10 @@ export function getCommandFilterList(data) {
export function getCategoryTypes() {
return request({
url: '/api/v1/assets/categories/',
url: '/api/v1/assets/categories/?limit=1000',
method: 'get'
}).then(res => {
return res.results
})
}

View File

@@ -52,3 +52,22 @@ export function createJob(form) {
data: form
})
}
export function StopJob(form) {
return request({
url: '/api/v1/ops/job-executions/stop/',
method: 'post',
data: form
})
}
export function JobUploadFile(form) {
return request({
url: '/api/v1/ops/jobs/upload/',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60 * 60 * 1000,
data: form
})
}

View File

@@ -12,7 +12,6 @@ export function getProfile(token) {
return request({
url: '/api/v1/users/profile/',
method: 'get'
// params: { token }
})
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="transparent" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot "><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,191 @@
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
export const accountFieldsMeta = (vm) => {
const defaultPrivilegedAccounts = ['root', 'administrator']
return {
assets: {
rules: [Required],
component: AssetSelect,
label: vm.$t('assets.Asset'),
el: {
multiple: false
},
hidden: () => {
return vm.platform || vm.asset
}
},
template: {
component: Select2,
rules: [Required],
el: {
multiple: false,
ajax: {
url: '/api/v1/accounts/account-templates/',
transformOption: (item) => {
return { label: item.name, value: item.id }
}
}
},
hidden: () => {
return vm.platform || vm.asset || !vm.addTemplate
}
},
on_invalid: {
rules: [Required],
label: vm.$t('accounts.AccountPolicy'),
helpText: vm.$t('accounts.BulkCreateStrategy'),
hidden: () => {
return vm.platform || vm.asset
}
},
name: {
label: vm.$t('common.Name'),
rules: [RequiredChange],
on: {
input: ([value], updateForm) => {
if (!vm.usernameChanged) {
if (!vm.account?.name) {
updateForm({ username: value })
}
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
}
},
hidden: () => {
return vm.addTemplate
}
},
username: {
el: {
disabled: !!vm.account?.name
},
on: {
input: ([value], updateForm) => {
vm.usernameChanged = true
},
change: ([value], updateForm) => {
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
},
hidden: () => {
return vm.addTemplate
}
},
privileged: {
label: vm.$t('assets.Privileged'),
hidden: () => {
return vm.addTemplate
}
},
su_from: {
component: Select2,
hidden: (formValue) => {
return !vm.asset?.id || !vm.iPlatform.su_enabled
},
el: {
multiple: false,
clearable: true,
ajax: {
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${vm.account?.id || ''}&asset=${vm.asset?.id || ''}`,
transformOption: (item) => {
return { label: `${item.name}(${item.username})`, value: item.id }
}
}
}
},
su_from_username: {
label: vm.$t('assets.UserSwitchFrom'),
hidden: (formValue) => {
return vm.platform || vm.asset || vm.addTemplate
}
},
password: {
label: vm.$t('assets.Password'),
component: UpdateToken,
hidden: (formValue) => {
return formValue.secret_type !== 'password' || vm.addTemplate
}
},
ssh_key: {
label: vm.$t('assets.PrivateKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
},
passphrase: {
label: vm.$t('assets.Passphrase'),
component: UpdateToken,
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
},
token: {
label: vm.$t('assets.Token'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'token' || vm.addTemplate
},
access_key: {
id: 'access_key',
label: vm.$t('assets.AccessKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'access_key' || vm.addTemplate
},
api_key: {
id: 'api_key',
label: vm.$t('assets.ApiKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'api_key' || vm.addTemplate
},
secret_type: {
type: 'radio-group',
options: [],
hidden: () => {
return vm.addTemplate
}
},
push_now: {
helpText: vm.$t('accounts.AccountPush.WindowsPushHelpText'),
hidden: (formValue) => {
const automation = vm.iPlatform.automation || {}
return !automation.push_account_enabled ||
!automation.ansible_enabled ||
!vm.$hasPerm('accounts.push_account') ||
(formValue.secret_type === 'ssh_key' && vm.iPlatform.type.value === 'windows') ||
vm.addTemplate
}
},
params: {
label: vm.$t('assets.PushParams'),
component: AutomationParamsForm,
el: {},
hidden: (formValue) => {
const automation = vm.iPlatform.automation || {}
vm.fieldsMeta.params.el.method = vm.iPlatform.automation.push_account_method
vm.fieldsMeta.params.el.pushAccountParams = vm.iPlatform.automation.push_account_params
return !formValue.push_now ||
!automation.push_account_enabled ||
!automation.ansible_enabled ||
(formValue.secret_type === 'ssh_key' &&
vm.iPlatform.type.value === 'windows') ||
!vm.$hasPerm('accounts.push_account') ||
vm.addTemplate
}
},
is_active: {
label: vm.$t('common.IsActive')
},
comment: {
label: vm.$t('common.Comment'),
hidden: () => {
return vm.addTemplate
}
}
}
}

View File

@@ -9,12 +9,8 @@
<script>
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
import { encryptPassword } from '@/utils/crypto'
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
export default {
name: 'AccountCreateForm',
@@ -48,7 +44,6 @@ export default {
return {
loading: true,
usernameChanged: false,
defaultPrivilegedAccounts: ['root', 'administrator'],
iPlatform: {
automation: {},
su_enabled: false,
@@ -72,178 +67,7 @@ export default {
]],
[this.$t('common.Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
],
fieldsMeta: {
assets: {
rules: [Required],
component: AssetSelect,
label: this.$t('assets.Asset'),
el: {
multiple: false
},
hidden: () => {
return this.platform || this.asset
}
},
template: {
component: Select2,
rules: [Required],
el: {
multiple: false,
ajax: {
url: '/api/v1/accounts/account-templates/',
transformOption: (item) => {
return { label: item.name, value: item.id }
}
}
},
hidden: () => {
return this.platform || this.asset || !this.addTemplate
}
},
on_invalid: {
rules: [Required],
label: this.$t('accounts.AccountPolicy'),
helpText: this.$t('accounts.BulkCreateStrategy'),
hidden: () => {
return this.platform || this.asset
}
},
name: {
rules: [RequiredChange],
on: {
input: ([value], updateForm) => {
if (!this.usernameChanged) {
if (!this.account?.name) {
updateForm({ username: value })
}
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
}
},
hidden: () => {
return this.addTemplate
}
},
username: {
el: {
disabled: !!this.account?.name
},
on: {
input: ([value], updateForm) => {
this.usernameChanged = true
},
change: ([value], updateForm) => {
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
},
hidden: () => {
return this.addTemplate
}
},
privileged: {
hidden: () => {
return this.addTemplate
}
},
su_from: {
component: Select2,
hidden: (formValue) => {
return !this.asset?.id || !this.iPlatform.su_enabled
},
el: {
multiple: false,
clearable: true,
ajax: {
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${this.account?.id || ''}&asset=${this.asset?.id || ''}`,
transformOption: (item) => {
return { label: `${item.name}(${item.username})`, value: item.id }
}
}
}
},
su_from_username: {
label: this.$t('assets.UserSwitchFrom'),
hidden: (formValue) => {
return this.platform || this.asset || this.addTemplate
}
},
password: {
label: this.$t('assets.Password'),
component: UpdateToken,
hidden: (formValue) => formValue.secret_type !== 'password' || this.addTemplate
},
ssh_key: {
label: this.$t('assets.PrivateKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
},
passphrase: {
label: this.$t('assets.Passphrase'),
component: UpdateToken,
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
},
token: {
label: this.$t('assets.Token'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'token' || this.addTemplate
},
access_key: {
id: 'access_key',
label: this.$t('assets.AccessKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'access_key' || this.addTemplate
},
api_key: {
id: 'api_key',
label: this.$t('assets.ApiKey'),
component: UploadSecret,
hidden: (formValue) => formValue.secret_type !== 'api_key' || this.addTemplate
},
secret_type: {
type: 'radio-group',
options: [],
hidden: () => {
return this.addTemplate
}
},
push_now: {
helpText: this.$t('accounts.AccountPush.WindowsPushHelpText'),
hidden: (formValue) => {
const automation = this.iPlatform.automation || {}
return !automation.push_account_enabled ||
!automation.ansible_enabled ||
!this.$hasPerm('accounts.push_account') ||
(formValue.secret_type === 'ssh_key' && this.iPlatform.type.value === 'windows') ||
this.addTemplate
}
},
params: {
label: this.$t('assets.PushParams'),
component: AutomationParamsForm,
el: {
method: this.asset?.auto_config?.push_account_method
},
hidden: (formValue) => {
const automation = this.iPlatform.automation || {}
return !formValue.push_now ||
!automation.push_account_enabled ||
!automation.ansible_enabled ||
!this.$hasPerm('accounts.push_account') ||
this.addTemplate
}
},
comment: {
hidden: () => {
return this.addTemplate
}
}
},
fieldsMeta: accountFieldsMeta(this),
hasSaveContinue: false
}
},
@@ -251,11 +75,18 @@ export default {
try {
await this.getPlatform()
this.setSecretTypeOptions()
this.getDefaultAssets()
} finally {
this.loading = false
}
},
methods: {
async getDefaultAssets() {
const assetId = this.$route.query.asset_id
if (assetId && !this.form.name) {
this.form.assets = [assetId]
}
},
async getPlatform() {
if (this.platform) {
this.iPlatform = this.platform

View File

@@ -0,0 +1,87 @@
<template>
<GenericUpdateFormDialog
v-if="visible"
:form-setting="formSetting"
:selected-rows="selectedRows"
:visible="visible"
v-on="$listeners"
/>
</template>
<script>
import { GenericUpdateFormDialog } from '@/layout/components'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'AccountBulkUpdateDialog',
components: {
GenericUpdateFormDialog
},
props: {
visible: {
type: Boolean,
default: false
},
selectedRows: {
type: Array,
default: () => ([])
}
},
data() {
return {
formSetting: {
url: '/api/v1/accounts/accounts/',
hasSaveContinue: false,
fields: [],
fieldsMeta: accountFieldsMeta(this),
cleanOtherFormValue: (formValue) => {
for (const value of formValue) {
Object.keys(value).forEach((item, index, arr) => {
if (['ssh_key', 'token', 'access_key', 'api_key', 'password'].includes(item)) {
value['secret'] = encryptPassword(value[item])
delete value[item]
}
})
}
return formValue
}
}
}
},
created() {
this.filterFieldsMeta()
},
methods: {
filterFieldsMeta() {
let fields = ['privileged']
const fieldsMeta = {}
const secretFields = ['password', 'ssh_key', 'passphrase', 'token', 'access_key', 'api_key']
const secret_type = this.selectedRows[0].secret_type?.value || 'password'
for (const field of secretFields) {
if (secret_type === 'ssh_key' && field === 'passphrase') {
fields.push('passphrase')
this.formSetting.fieldsMeta['passphrase'].hidden = () => false
continue
}
if (secret_type === field) {
fields.push(field)
this.formSetting.fieldsMeta[field].hidden = () => false
continue
}
delete this.formSetting.fieldsMeta[field]
}
fields = fields.concat(['is_active', 'comment'])
for (const field of fields) {
fieldsMeta[field] = this.formSetting.fieldsMeta[field]
}
this.formSetting.fields = fields
this.formSetting.fieldsMeta = fieldsMeta
}
}
}
</script>
<style scoped>
</style>

View File

@@ -8,7 +8,6 @@
:title="title"
:visible.sync="iVisible"
v-bind="$attrs"
width="70%"
v-on="$listeners"
>
<AccountCreateUpdateForm

View File

@@ -37,6 +37,12 @@
:result="createAccountResults"
:visible.sync="showResultDialog"
/>
<AccountBulkUpdateDialog
v-if="updateSelectedDialogSetting.visible"
:visible.sync="updateSelectedDialogSetting.visible"
v-bind="updateSelectedDialogSetting"
@update="handleAccountBulkUpdate"
/>
</div>
</template>
@@ -49,10 +55,12 @@ import AccountCreateUpdate from './AccountCreateUpdate.vue'
import { connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
import ResultDialog from './BulkCreateResultDialog.vue'
import AccountBulkUpdateDialog from '@/components/Apps/AccountListTable/AccountBulkUpdateDialog.vue'
export default {
name: 'AccountListTable',
components: {
AccountBulkUpdateDialog,
ResultDialog,
ListTable,
UpdateSecretInfo,
@@ -117,6 +125,10 @@ export default {
headerExtraActions: {
type: Array,
default: () => []
},
extraQuery: {
type: Object,
default: () => ({})
}
},
data() {
@@ -138,9 +150,7 @@ export default {
app: 'assets',
resource: 'account'
},
extraQuery: {
order: '-date_updated'
},
extraQuery: this.extraQuery,
columnsExclude: ['spec_info'],
columnsShow: {
min: ['name', 'username', 'actions'],
@@ -239,7 +249,7 @@ export default {
},
{
name: 'Test',
title: this.$t('common.Test'),
title: this.$t('accounts.Test'),
can: ({ row }) =>
!this.$store.getters.currentOrgIsRoot &&
this.$hasPerm('accounts.change_account') &&
@@ -279,6 +289,7 @@ export default {
}
},
headerActions: {
hasLabelSearch: true,
hasLeftActions: this.hasLeftActions,
hasMoreActions: true,
hasCreate: false,
@@ -338,6 +349,29 @@ export default {
...this.headerExtraActions
],
extraMoreActions: [
{
name: 'BulkVerify',
title: this.$t('accounts.BulkVerify'),
type: 'primary',
fa: 'fa-link',
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
!this.$store.getters.currentOrgIsRoot
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.post(
'/api/v1/accounts/accounts/tasks/',
{ action: 'verify', accounts: ids }).then(res => {
openTaskPage(res['task'])
}).catch(err => {
this.$message.error(this.$tc('common.bulkVerifyErrorMsg' + ' ' + err))
})
}.bind(this)
},
{
name: 'ClearSecrets',
title: this.$t('common.ClearSecret'),
@@ -347,7 +381,9 @@ export default {
return selectedRows.length > 0 && vm.$hasPerm('accounts.change_account')
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => { return v.id })
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.patch(
'/api/v1/accounts/accounts/clear-secret/',
{ account_ids: ids }).then(() => {
@@ -356,6 +392,21 @@ export default {
this.$message.error(this.$tc('common.bulkClearErrorMsg' + ' ' + err))
})
}.bind(this)
},
{
name: 'actionUpdateSelected',
title: this.$t('accounts.AccountBatchUpdate'),
fa: 'batch-update',
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
!this.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.change_account') &&
selectedRows.every(i => i.secret_type.value === selectedRows[0].secret_type.value)
},
callback: ({ selectedRows }) => {
vm.updateSelectedDialogSetting.selectedRows = selectedRows
vm.updateSelectedDialogSetting.visible = true
}
}
],
canBulkDelete: vm.$hasPerm('accounts.delete_account'),
@@ -364,6 +415,10 @@ export default {
exclude: ['asset']
},
hasSearch: true
},
updateSelectedDialogSetting: {
visible: false,
selectedRows: []
}
}
},
@@ -391,9 +446,18 @@ export default {
can: this.$hasPerm('accounts.delete_account'),
type: 'primary',
callback: ({ row }) => {
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
const msg = this.$t('accounts.AccountDeleteConfirmMsg')
this.$confirm(msg, this.$tc('common.Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
done()
this.$refs.ListTable.reloadTable()
this.$message.success(this.$tc('common.deleteSuccessMsg'))
})
}
})
}
}
@@ -422,6 +486,10 @@ export default {
setTimeout(() => {
this.showResultDialog = true
}, 100)
},
handleAccountBulkUpdate() {
this.updateSelectedDialogSetting.visible = false
this.$refs.ListTable.reloadTable()
}
}
}

View File

@@ -0,0 +1,112 @@
<template>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
:visible.sync="show"
:width="'50'"
v-bind="$attrs"
@confirm="accountConfirmHandle"
v-on="$listeners"
/>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'RemoveAccount',
components: {
Dialog
},
props: {
accounts: {
type: Array,
default: () => []
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
show: false,
mfaDialogVisible: true
}
},
computed: {},
mounted() {
const url = `/api/v1/accounts/accounts/tasks/`
this.$axios.post(
url, { disableFlashErrorMsg: true, action: 'remove' }
).then(resp => {
this.$axios.post(
`/api/v1/accounts/accounts/tasks/`,
{
action: 'remove',
gather_accounts: this.accounts.map(account => account.id)
}
).then(res => {
openTaskPage(res['task'])
})
})
},
methods: {
accountConfirmHandle() {
this.show = false
this.mfaDialogVisible = false
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.item-textarea > > > .el-textarea__inner {
height: 110px;
}
.el-form-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
> > > .el-form-item__label {
padding-right: 20px;
line-height: 30px;
}
> > > .el-form-item__content {
line-height: 30px;
pre {
margin: 0;
}
}
}
ul {
margin: 0;
}
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
}
}
</style>

View File

@@ -83,6 +83,10 @@ export default {
type: String,
default: ''
},
type: {
type: String,
default: 'account'
},
title: {
type: String,
default: function() {
@@ -136,7 +140,8 @@ export default {
name: this.secretInfo.name,
secret: encryptPassword(this.modifiedSecret)
}
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, params).then(() => {
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
})
},

View File

@@ -1,10 +1,11 @@
<template>
<Dialog
:close-on-click-modal="false"
:title="$tc('assets.Assets')"
custom-class="asset-select-dialog"
top="1vh"
top="2vh"
v-bind="$attrs"
width="80vw"
width="1000px"
@cancel="handleCancel"
@close="handleClose"
@confirm="handleConfirm"
@@ -17,6 +18,7 @@
:table-config="tableConfig"
:tree-url="`${baseNodeUrl}children/tree/`"
:url="baseUrl"
:tree-setting="treeSetting"
class="tree-table"
v-bind="$attrs"
/>
@@ -52,6 +54,10 @@ export default {
disabled: {
type: [Boolean, Function],
default: false
},
treeSetting: {
type: Object,
default: () => ({})
}
},
data() {
@@ -83,16 +89,6 @@ export default {
return row.platform.name
}
},
{
prop: 'protocols',
formatter: function(row) {
const data = row.protocols.map(p => {
return <el-tag size='mini'>{p.name}/{p.port} </el-tag>
})
return <span> {data} </span>
},
label: this.$t('assets.Protocols')
},
{
prop: 'actions',
has: false
@@ -114,6 +110,7 @@ export default {
headerActions: {
hasLeftActions: false,
hasRightActions: false,
hasLabelSearch: true,
searchConfig: {
getUrlQuery: false
}
@@ -168,7 +165,7 @@ export default {
}
.right {
height: calc(100vh - 200px);
min-height: 500px;
overflow: auto;
}

View File

@@ -13,6 +13,7 @@
ref="dialog"
:base-node-url="baseNodeUrl"
:base-url="baseUrl"
:tree-setting="treeSetting"
:tree-url-query="treeUrlQuery"
:value="value"
:visible.sync="dialogVisible"
@@ -48,6 +49,10 @@ export default {
value: {
type: Array,
default: () => []
},
treeSetting: {
type: Object,
default: () => ({})
}
},
data() {
@@ -134,7 +139,8 @@ export default {
padding: 5px;
.ztree {
height: calc(100vh - 250px) !important;
min-height: 500px;
height: inherit !important;
}
}

View File

@@ -60,6 +60,7 @@ export default {
const showAssets = this.treeSetting?.showAssets || this.showAssets
const treeUrlQuery = this.setTreeUrlQuery()
const assetTreeUrl = `${this.treeUrl}?assets=${showAssets ? '1' : '0'}&${treeUrlQuery}`
const vm = this
return {
treeTabConfig: {
@@ -81,7 +82,13 @@ export default {
nodeUrl: this.nodeUrl,
treeUrl: assetTreeUrl,
callback: {
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode),
beforeRefresh: () => {
const query = { ...this.$route.query, node_id: '', asset_id: '' }
setTimeout(() => {
vm.$router.replace({ query: query })
}, 100)
}
},
...this.treeSetting
}

View File

@@ -5,10 +5,12 @@
size="mini"
type="primary"
@click="onOpenDialog"
>{{ $tc('common.View') }}</el-button>
>
{{ $tc('common.View') }}
<span>({{ $tc('setting.LockedIP', ipCounts ) }})</span>
</el-button>
</div>
<Dialog
v-if="visible"
:visible.sync="visible"
:title="title"
width="40%"
@@ -54,6 +56,7 @@ export default {
remoteMeta: {},
visible: false,
form: this.value,
ipCounts: 0,
config: {
url: this.url,
hasSaveContinue: false,
@@ -63,7 +66,15 @@ export default {
}
}
},
created() {
this.getLockedIp()
},
methods: {
getLockedIp() {
this.$axios.get('/api/v1/settings/security/block-ip/').then(res => {
this.ipCounts = res.count
})
},
onOpenDialog() {
this.visible = true
}

View File

@@ -0,0 +1,136 @@
<template>
<div>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
:title="title"
:visible.sync="showSecret"
:width="'50'"
v-bind="$attrs"
@confirm="accountConfirmHandle"
v-on="$listeners"
>
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
<el-form-item :label="$tc('accounts.AccountChangeSecret.OldSecret')">
<ShowKeyCopyFormatter
:cell-value="secretInfo.old_secret"
:col="{ formatterArgs: {
name: 'old_secret'
}}"
/>
</el-form-item>
<el-form-item :label="$tc('accounts.AccountChangeSecret.NewSecret')">
<ShowKeyCopyFormatter
:cell-value="secretInfo.new_secret"
:col="{ formatterArgs: {
name: 'new_secret'
}}"
/>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'RecordViewSecret',
components: {
Dialog,
ShowKeyCopyFormatter
},
props: {
visible: {
type: Boolean,
default: false
},
url: {
type: String,
default: ''
},
title: {
type: String,
default: function() {
return this.$tc('common.ViewSecret')
}
}
},
data() {
return {
secretInfo: {},
showSecret: false,
mfaDialogVisible: true
}
},
computed: {
},
mounted() {
this.showSecretDialog()
},
methods: {
accountConfirmHandle() {
this.showSecret = false
this.mfaDialogVisible = false
},
showSecretDialog() {
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
this.secretInfo = res
this.showSecret = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.item-textarea >>> .el-textarea__inner {
height: 110px;
}
.el-form-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
>>> .el-form-item__label {
padding-right: 20px;
line-height: 30px;
}
>>> .el-form-item__content {
line-height: 30px;
pre {
margin: 0;
}
}
}
ul {
margin: 0;
}
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div class="container">
<div class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
v-bind="select"
@change="onSelectChange"
/>
</div>
<div class="chat-input">
<el-input
v-model="inputValue"
:disabled="isLoading"
:placeholder="$tc('common.InputMessage')"
type="textarea"
@compositionend="isIM = false"
@compositionstart="isIM = true"
@keypress.native="onKeyEnter"
/>
<div class="input-action">
<span class="right">
<i :class="{'active': inputValue }" class="fa fa-send" @click="onSendHandle" />
</span>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Select2 from '../../../../Form/FormFields/Select2.vue'
import { useChat } from '../../useChat.js'
const { setLoading } = useChat()
export default {
components: { Select2 },
props: {
},
data() {
return {
isIM: false,
inputValue: '',
select: {
url: '/api/v1/settings/chatai-prompts/',
value: '',
multiple: false,
placeholder: this.$t('common.Prompt'),
ajax: {
transformOption: (item) => {
return { label: item.name, value: item.content }
}
}
}
}
},
computed: {
...mapState({
isLoading: state => state.chat.loading
}),
isSelectDisabled() {
return !!this.select.value
}
},
methods: {
onKeyEnter(event) {
if (event.key === 'Enter') {
if ((!this.isIM && !event.shiftKey) || (this.isIM && event.ctrlKey)) {
event.preventDefault()
this.onSendHandle()
}
}
},
onSendHandle() {
if (!this.inputValue) return
setLoading(true)
this.$emit('send', this.inputValue)
this.inputValue = ''
},
onSelectChange(value) {
this.$emit('select-prompt', value)
}
}
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
height: 100%;
flex-direction: column;
.chat-action {
width: 100%;
margin: 6px 0;
&>>> .el-select {
width: 50%;
.el-input__inner {
height: 28px;
line-height: 28px;
border-radius: 14px;
border-color: transparent;
background-color: #f7f7f8;
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
&:hover {
background-color: #ededed;
}
}
.el-input__icon {
line-height: 0px;
}
}
}
.chat-input {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #DCDFE6;
border-radius: 12px;
&:has(.el-textarea__inner:focus) {
border: 1px solid var(--color-primary);
}
&>>> .el-textarea {
height: 100%;
.el-textarea__inner {
height: 100%;
padding: 8px 10px;
border: none;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
resize: none;
&::-webkit-scrollbar {
width: 12px;
}
}
}
.el-textarea.is-disabled + .input-action {
background-color: #F5F7FA;
cursor: no-drop;
i {
cursor: no-drop;
}
}
.input-action {
overflow: hidden;
padding: 0 16px 15px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
.right {
float: right;
.active {
color: var(--color-primary);
}
i {
cursor: pointer;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div :class="{'user-role': isUserRole}" class="chat-item">
<div class="avatar">
<el-avatar :src="isUserRole ? userUrl : chatUrl" class="header-avatar" />
</div>
<div class="content">
<div class="operational">
<span class="date">
{{ $moment(item.message.create_time).format('YYYY-MM-DD HH:mm:ss') }}
</span>
</div>
<div class="message">
<div class="message-content">
<span v-if="isSystemError" class="error">
{{ item.message.content }}
</span>
<span v-else class="chat-text">
<MessageText :message="item.message" />
</span>
</div>
<div class="action">
<el-tooltip
v-if="isSystemError && isLoading"
:content="$tc('common.Reconnect')"
effect="dark"
placement="top"
>
<svg-icon icon-class="refresh" @click="onRefresh" />
</el-tooltip>
<el-dropdown v-else size="small" @command="handleCommand">
<span class="el-dropdown-link">
<i class="fa fa-ellipsis-v" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="i in dropdownOptions" :key="i.action" :command="i.action">
{{ i.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
</div>
</template>
<script>
import MessageText from './MessageText.vue'
import { mapState } from 'vuex'
import { copy } from '@/utils/common'
import { useChat } from '../../useChat.js'
import { reconnect } from '@/utils/socket'
const { setLoading, removeLoadingMessageInChat } = useChat()
export default {
components: {
MessageText
},
props: {
item: {
type: Object,
default: () => {}
}
},
data() {
return {
chatUrl: require('@/assets/img/chat.png'),
userUrl: '/api/v1/settings/logo/',
dropdownOptions: [
{
action: 'copy',
label: this.$t('common.Copy')
}
]
}
},
computed: {
...mapState({
isLoading: state => state.chat.loading
}),
isUserRole() {
return this.item.message?.role === 'user'
},
isSystemError() {
return this.item.type === 'error' && this.item.message?.role === 'assistant'
}
},
methods: {
onRefresh() {
reconnect()
removeLoadingMessageInChat()
setLoading(false)
},
handleCommand(value) {
if (value === 'copy') {
copy(this.item.message.content)
}
}
}
}
</script>
<style lang="scss" scoped>
.chat-item {
display: flex;
padding: 16px 14px 0;
&:last-child {
padding-bottom: 16px;
}
.avatar {
width: 22px;
height: 22px;
margin-top: 2px;
.header-avatar {
width: 100%;
height: 100%;
&>>> img {
background-color: #e5e5e7;
}
}
}
.content {
margin-left: 6px;
overflow: hidden;
.operational {
display: flex;
justify-content: space-between;
overflow: hidden;
.copy {
float: right;
cursor: pointer;
}
}
.message {
display: -webkit-box;
.message-content {
flex: 1;
padding: 6px 10px;
border-radius: 2px 12px 12px;
background-color: #f0f1f5;
}
.action {
.svg-icon {
transform: translateY(50%);
margin-left: 3px;
cursor: pointer;
}
.el-dropdown {
height: 32px;
line-height: 37px;
font-size: 13px;
.el-dropdown-link {
i {
padding: 4px 5px;
font-size: 15px;
color: #8d9091;
&:hover {
color: #7b8085
}
}
}
}
}
.error {
color: red;
}
}
}
}
.user-role {
flex-direction: row-reverse;
.content {
margin-right: 10px;
.operational {
flex-direction: row-reverse;
}
.message {
flex-direction: row-reverse;
.message-content {
background-color: var(--menu-hover);
border-radius: 12px 2px 12px 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<div>
<div ref="textRef" class="leading-relaxed break-words">
<span v-if="message.content === 'loading'" class="loading-box">
<span />
<span />
<span />
</span>
<div v-else class="inline-block markdown-body" v-html="text" />
</div>
</div>
</template>
<script>
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import { copy } from '@/utils/common'
/* eslint-disable vue/no-v-html */
export default {
props: {
message: {
type: Object,
default: () => {}
}
},
data() {
return {
markdown: null
}
},
computed: {
text() {
const value = this.message?.content || ''
if (value && this.markdown) {
return this.markdown?.render(value)
}
return this.$xss.process(value)
}
},
mounted() {
this.init()
},
updated() {
this.addCopyEvents()
},
destroyed() {
this.removeCopyEvents()
},
methods: {
init() {
const vm = this
this.markdown = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language || ''
return vm.highlightBlock(hljs.highlight(lang, code, true).value, lang)
}
return vm.highlightBlock(hljs.highlightAuto(code).value, '')
}
})
this.markdown.use(mila, { attrs: { target: '_blank', rel: 'noopener', class: 'link-style' }})
this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' })
},
highlightBlock(str, lang) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${'Copy'}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
},
addCopyEvents() {
const copyBtn = document.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copy(code)
}
})
})
},
removeCopyEvents() {
if (this.$refs.textRef) {
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {})
})
}
}
}
}
</script>
<style lang="scss" scoped>
.markdown-body {
font-size: 13px;
&>>> p {
margin-bottom: 0 !important;
}
background: inherit;
&>>> pre {
padding: 0 0 6px 0;
.hljs.code-block-body {
border-radius: 4px;
}
}
&>>> .code-block-wrapper {
background: #1F2329;
padding: 2px 6px;
margin: 5px 0;
.code-block-body {
padding: 5px 10px 0;
};
.code-block-header {
margin-bottom: 4px;
overflow: hidden;
background: #353946;
color: #c2d1e1;
.code-block-header__copy {
float: right;
cursor: pointer;
&:hover {
color: #6e747b;
}
}
}
.hljs.code-block-body.javascript {
.hljs-comment {
display: block;
}
}
}
}
>>> .link-style {
color: #487bf4;
&:hover {
color: #275ee3;
}
}
.loading-box{
margin-left: 6px;
}
.loading-box span{
display: inline-block;
width: 5px;
height: 5px;
margin-right: 5px;
border-radius: 50%;
vertical-align: middle;
background: #676A6c;
animation: load 1.2s ease infinite;
}
.loading-box span:last-child{
margin-right: 0;
}
@keyframes load{
0%{
opacity: 1;
}
100%{
opacity: 0;
}
}
.loading-box span:nth-child(1){
animation-delay: 0.23s;
}
.loading-box span:nth-child(2){
animation-delay: 0.36s;
}
.loading-box span:nth-child(3){
animation-delay: 0.49s;
}
</style>

View File

@@ -0,0 +1,271 @@
<template>
<div class="chat-content">
<div id="scrollRef" class="chat-list">
<div v-if="showIntroduction" class="introduction">
<div v-for="(item, index) in introduction" :key="index" class="introduction-item" @click="sendIntroduction(item)">
<div class="head">
<i v-if="item.icon" :class="item.icon" />
<span class="title">{{ item.title }}</span>
</div>
<div class="content">
{{ item.content }}
</div>
</div>
</div>
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" />
</div>
<div class="input-box">
<el-button
v-if="isLoading && socket && socket.readyState === 1"
class="stop"
icon="fa fa-stop-circle-o"
round
size="small"
@click="onStopHandle"
>{{ $tc('common.Stop') }}</el-button>
<ChatInput ref="chatInput" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
</div>
</div>
</template>
<script>
import ChatInput from './ChatInput.vue'
import ChatMessage from './ChatMessage.vue'
import { mapState } from 'vuex'
import { closeWebSocket, createWebSocket, onSend, ws } from '@/utils/socket'
import { getInputFocus, useChat } from '../../useChat.js'
const {
setLoading,
clearChats,
addChatMessageById,
addMessageToActiveChat,
newChatAndAddMessageById,
removeLoadingMessageInChat,
updateChaMessageContentById,
addTemporaryLoadingToChat
} = useChat()
export default {
components: {
ChatInput,
ChatMessage
},
props: {
},
data() {
return {
socket: {},
prompt: '',
currentConversationId: '',
showIntroduction: false,
introduction: [
{
title: this.$t('common.introduction.ConceptTitle'),
content: this.$t('common.introduction.ConceptContent')
},
{
title: this.$t('common.introduction.IdeaTitle'),
content: this.$t('common.introduction.IdeaContent')
}
]
}
},
computed: {
...mapState({
isLoading: state => state.chat.loading,
activeChat: state => state.chat.activeChat
})
},
destroyed() {
closeWebSocket()
},
methods: {
init() {
this.initWebSocket()
this.initChatMessage()
},
initWebSocket() {
const { NODE_ENV, VUE_APP_KAEL_HOST } = process.env || {}
const api = '/kael/chat/system/'
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const path = `${protocol}://${window.location.host}${api}`
const index = VUE_APP_KAEL_HOST?.indexOf('://')
const localPath = protocol + VUE_APP_KAEL_HOST?.substring(index, VUE_APP_KAEL_HOST?.length) + api
const url = NODE_ENV === 'development' ? localPath : path
createWebSocket(url, this.onWebSocketMessage)
},
initChatMessage() {
this.prompt = ''
this.showIntroduction = true
this.currentConversationId = ''
this.$refs.chatInput.select.value = ''
const chat = {
message: {
content: this.$t('common.ChatHello'),
role: 'assistant',
create_time: new Date()
}
}
newChatAndAddMessageById(chat)
setLoading(false)
},
onWebSocketMessage(data) {
if (data.type === 'message') {
this.onChatMessage(data)
}
if (data.type === 'error') {
this.onSystemMessage(data)
}
},
onChatMessage(data) {
if (data.conversation_id) {
setLoading(true)
removeLoadingMessageInChat()
this.currentConversationId = data.conversation_id
updateChaMessageContentById(data.message.id, data)
}
if (data.message?.type === 'finish') {
setLoading(false)
getInputFocus()
}
},
onSystemMessage(data) {
data.message = {
content: data.system_message,
role: 'assistant',
create_time: new Date()
}
removeLoadingMessageInChat()
addMessageToActiveChat(data)
this.socketReadyStateSuccess = false
setLoading(true)
},
onSendHandle(value) {
this.showIntroduction = false
this.socket = ws || {}
if (ws?.readyState === 1) {
this.socketReadyStateSuccess = true
const chat = {
message: {
content: value,
role: 'user',
create_time: new Date()
}
}
const message = {
content: value,
prompt: this.prompt,
conversation_id: this.currentConversationId || ''
}
addChatMessageById(chat)
onSend(message)
addTemporaryLoadingToChat()
} else {
const chat = {
message: {
content: this.$t('common.ConnectionDropped'),
role: 'assistant',
create_time: new Date()
},
type: 'error'
}
addChatMessageById(chat)
this.socketReadyStateSuccess = false
setLoading(true)
}
},
onSelectPromptHandle(value) {
this.prompt = value
this.currentConversationId = ''
this.showIntroduction = false
this.onSendHandle(value)
},
onNewChat() {
clearChats()
this.initChatMessage()
},
onStopHandle() {
this.$axios.post(
'/kael/interrupt_current_ask/',
{ id: this.currentConversationId || '' }
).finally(() => {
removeLoadingMessageInChat()
setLoading(false)
})
},
sendIntroduction(item) {
this.showIntroduction = false
this.onSendHandle(item.content)
}
}
}
</script>
<style lang="scss" scoped>
.chat-content {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.introduction {
padding: 16px 14px 0;
.introduction-item {
padding: 12px 14px;
border-radius: 8px;
margin-top: 16px;
background-color: var(--menu-hover);
cursor: pointer;
&:hover {
box-shadow: 0 0 2px 2px #00000014;
}
&:first-child {
margin-top: 0;
}
.head {
margin-bottom: 2px;
.title {
font-weight: 500;
color: #373739;
}
}
.content {
display: inline-block;
color: #a7a7ab;
word-wrap: break-word;
}
}
}
.chat-list {
flex: 1;
position: relative;
padding: 0 15px 25px;
overflow-y: auto;
user-select: text;
&::-webkit-scrollbar {
width: 12px;
}
}
.input-box {
position: relative;
height: 160px;
padding: 0 15px;
margin-bottom: 15px;
border-top: 1px solid #ececec;
}
.stop {
position: absolute;
top: -37px;
left: 50%;
z-index: 11;
transform: translateX(-50%);
>>> i {
margin-right: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="container">
<div class="close-sidebar">
<i v-if="hasClose" class="el-icon-close" @click="onClose" />
</div>
<el-tabs v-model="active" :tab-position="'right'" @tab-click="handleClick">
<el-tab-pane v-for="(item) in submenu" :key="item.name" :name="item.name">
<span slot="label">
<el-tooltip effect="dark" placement="left" :content="item.label">
<svg-icon :icon-class="item.icon" />
</el-tooltip>
</span>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
export default {
props: {
active: {
type: String,
default: 'chat'
},
hasClose: {
type: Boolean,
default: true
},
submenu: {
type: Array,
default: () => []
}
},
data() {
return {
}
},
methods: {
handleClick(tab, event) {
this.$emit('tab-click', tab)
},
onClose() {
this.$parent.onClose()
}
}
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
background-color: #f0f1f5;
.close-sidebar {
height: 48px;
padding: 12px 0;
text-align: center;
font-size: 14px;
cursor: pointer;
i {
font-size: 16px;
font-weight: 600;
padding: 4px;
border-radius: 2px;
&:hover {
color: var(--color-primary);
background: var(--menu-hover);
}
}
}
}
>>> .el-tabs {
.el-tabs__item {
padding: 0 13px;
font-size: 15px;
:hover {
color: #7b8085;
}
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="chat">
<div class="container">
<div class="header">
<div class="left">
<img :src="robotUrl" alt="">
<span class="title">{{ title }}</span>
</div>
<span class="new" @click="onNewChat">
<i class="el-icon-plus" />
<span>{{ $tc('common.NewChat') }}</span>
</span>
</div>
<div class="content">
<keep-alive>
<component :is="active" ref="component" />
</keep-alive>
</div>
</div>
<div class="sidebar">
<Sidebar v-bind="$attrs" :active.sync="active" :submenu="submenu" />
</div>
</div>
</template>
<script>
import Sidebar from './components/Sidebar/index.vue'
import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/socket'
export default {
components: {
Chat,
Sidebar
},
props: {
title: {
type: String,
default: function() {
return this.$t('setting.ChatAI')
}
},
drawerPanelVisible: {
type: Boolean,
default: () => false
}
},
data() {
return {
active: 'chat',
robotUrl: require('../../../assets/img/robot-assistant.png'),
submenu: [
{
name: 'chat',
label: this.$t('common.Chat'),
icon: 'chat'
}
]
}
},
watch: {
drawerPanelVisible(value) {
if (value && !ws) {
this.initWebSocket()
}
}
},
methods: {
initWebSocket() {
this.$refs.component?.init()
},
onClose() {
this.$parent.show = false
},
onNewChat() {
this.active = 'chat'
this.$nextTick(() => {
this.$refs.component?.onNewChat()
getInputFocus()
})
}
}
}
</script>
<style lang="scss" scoped>
.chat {
display: flex;
width: 100%;
height: 100%;
.container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.header {
display: flex;
justify-content: space-between;
height: 48px;
line-height: 48px;
padding: 0 16px;
overflow: hidden;
border-bottom: 1px solid #ececec;
.left {
img {
width: 22px;
height: 22px;
vertical-align: sub;
}
.title {
display: inline-block;
font-size: 18px;
color: black;
}
}
.new {
display: inline-block;
height: 28px;
line-height: 28px;
border-radius: 16px;
padding: 0 10px;
transform: translateY(32%);
color: var(--color-primary);
background-color: #f7f7f8;
cursor: pointer;
font-size: 13px;
&:hover {
background-color: #ededed;
}
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
.sidebar {
height: 100%;
width: 42px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
import store from '@/store'
import { pageScroll } from '@/utils/common'
export const getInputFocus = () => {
const dom = document.querySelector('.chat-input .el-textarea__inner')
setTimeout(() => dom?.focus(), 200)
}
export function useChat() {
const chatStore = {}
const setLoading = (loading) => {
store.commit('chat/setLoading', loading)
}
const onNewChat = (name) => {
const data = {
name: name || `new chat`,
id: 1,
conversation_id: '',
chats: []
}
store.commit('chat/addChatToStore', data)
}
const clearChats = () => {
store.commit('chat/clearChats')
}
const addMessageToActiveChat = (chat) => {
store.commit('chat/addMessageToActiveChat', chat)
}
const removeLoadingMessageInChat = () => {
store.commit('chat/removeLoadingMessageInChat')
}
const addChatMessageById = (chat) => {
store.commit('chat/addMessageToActiveChat', chat)
if (chat?.conversation_id) {
store.commit('chat/setActiveChatConversationId', chat.conversation_id)
}
pageScroll('scrollRef')
}
const addTemporaryLoadingToChat = () => {
const temporaryChat = {
message: {
content: 'loading',
role: 'assistant',
create_time: new Date()
}
}
addChatMessageById(temporaryChat)
}
const newChatAndAddMessageById = (chat) => {
onNewChat(chat.message.content)
addChatMessageById(chat)
}
const updateChaMessageContentById = (id, data) => {
store.commit('chat/updateChaMessageContentById', { id, data })
pageScroll('scrollRef')
}
return {
chatStore,
setLoading,
onNewChat,
clearChats,
getInputFocus,
addMessageToActiveChat,
newChatAndAddMessageById,
removeLoadingMessageInChat,
addChatMessageById,
addTemporaryLoadingToChat,
updateChaMessageContentById
}
}

View File

@@ -0,0 +1,209 @@
<template>
<div ref="drawer" :class="{show: show}" class="drawer">
<div :style="{'background-color': modal ? 'rgba(0, 0, 0, .3)' : 'transparent'}" class="modal" />
<div :style="{'width': width}" class="drawer-panel">
<div v-show="!show" ref="dragBox" class="handle-button">
<i v-if="icon.startsWith('fa') || icon.startsWith('el')" :class="show ? 'el-icon-close': icon" />
<img v-else :src="icon" alt="">
</div>
<div class="drawer-panel-item">
<slot :drawer-panel-visible="show" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DrawerPanel',
props: {
icon: {
type: String,
default: 'el-icon-setting'
},
width: {
type: String,
default: '440px'
},
modal: {
type: Boolean,
default: true
},
clickNotClose: {
type: Boolean,
default: false
}
},
data() {
return {
show: false
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
this.$emit('toggle', this.show)
}
},
mounted() {
this.init()
this.insertToBody()
},
beforeDestroy() {
const element = this.$refs.drawer
element.remove()
window.removeEventListener('click', this.closeSidebar)
},
methods: {
init() {
this.$nextTick(() => {
const dragBox = this.$refs.dragBox
const clientOffset = {}
dragBox.addEventListener('mousedown', (event) => {
const offsetX = dragBox.getBoundingClientRect().left
const offsetY = dragBox.getBoundingClientRect().top
const innerX = event.clientX - offsetX
const innerY = event.clientY - offsetY
clientOffset.clientX = event.clientX
clientOffset.clientY = event.clientY
document.onmousemove = function(event) {
dragBox.style.left = event.clientX - innerX + 'px'
dragBox.style.top = event.clientY - innerY + 'px'
const dragDivTop = window.innerHeight - dragBox.getBoundingClientRect().height
const dragDivLeft = window.innerWidth - dragBox.getBoundingClientRect().width
dragBox.style.left = dragDivLeft + 'px'
dragBox.style.left = '-48px'
if (dragBox.getBoundingClientRect().top <= 0) {
dragBox.style.top = '0px'
}
if (dragBox.getBoundingClientRect().top >= dragDivTop) {
dragBox.style.top = dragDivTop + 'px'
}
event.preventDefault()
event.stopPropagation()
}
document.onmouseup = function() {
document.onmousemove = null
document.onmouseup = null
}
}, false)
dragBox.addEventListener('mouseup', (event) => {
const clientX = event.clientX
const clientY = event.clientY
if (this.isDifferenceWithinThreshold(clientX, clientOffset.clientX) && this.isDifferenceWithinThreshold(clientY, clientOffset.clientY)) {
this.show = !this.show
}
})
})
},
isDifferenceWithinThreshold(num1, num2, threshold = 5) {
const difference = Math.abs(num1 - num2)
return difference <= threshold
},
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.drawer-panel')
if (!parent && evt.target.className === 'modal') {
this.show = false
}
},
insertToBody() {
const element = this.$refs.drawer
const body = document.querySelector('body')
body.insertBefore(element, body.firstChild)
}
}
}
</script>
<style lang="scss" scoped>
.modal {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .3);
z-index: -1;
}
.drawer-panel {
position: fixed;
top: 0;
right: 0;
width: 100%;
min-width: 260px;
height: 100vh;
user-select: none;
transition: transform .25s cubic-bezier(.7, .3, .1, 1);
box-shadow: 0 0 8px 4px #00000014;
transform: translate(100%);
background: #FFFFFF;
z-index: 1200;
}
.drawer-panel-item {
height: 100%;
}
.drawer-panel-item::-webkit-scrollbar-track {
box-shadow: none;
background-color: transparent;
}
.show {
transition: all .3s cubic-bezier(.7, .3, .1, 1);
}
.show .modal {
z-index: 1003;
opacity: 1;
width: 100%;
height: 100%;
}
.show .drawer-panel {
transform: translate(0);
}
.handle-button {
position: absolute;
bottom: 20%;
left: -48px;
width: 48px;
height: 45px;
line-height: 45px;
box-sizing: border-box;
text-align: center;
font-size: 24px;
border-radius: 20px 0 0 20px;
z-index: 0;
pointer-events: auto;
color: #fff;
background-color: #FFFFFF;
box-shadow: 0 0 8px 4px #00000014;
cursor: pointer;
&:hover {
left: -50px !important;
width: 50px !important;
transform: scale(1.06);
}
i {
font-size: 20px;
line-height: 45px;
}
img {
width: 22px;
height: 22px;
transform: translateY(10%);
margin-left: 3px;
}
}
</style>

View File

@@ -5,6 +5,8 @@
<script type="text/jsx">
import TreeTable from '../../Table/TreeTable/index.vue'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import { AccountInfoFormatter } from '@/components/Table/TableFormatters'
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
export default {
name: 'GrantedAssets',
@@ -57,10 +59,11 @@ export default {
tableConfig: {
url: this.tableUrl,
hasTree: true,
columnsExtra: ['view_account'],
columnsExclude: ['spec_info'],
columnShow: {
columnsShow: {
min: ['name', 'address', 'accounts'],
default: ['name', 'address', 'accounts', 'actions']
default: ['name', 'address', 'platform', 'view_account', 'connectivity']
},
columnsMeta: {
name: {
@@ -71,7 +74,13 @@ export default {
},
actions: {
has: false
}
},
view_account: {
label: this.$t('assets.Account'),
formatter: AccountInfoFormatter,
width: '100px'
},
connectivity: connectivityMeta
}
},
headerActions: {

View File

@@ -1,7 +1,7 @@
<template>
<Dialog
:close-on-click-modal="false"
:destory-on-close="true"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="title"
@@ -51,7 +51,12 @@
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; margin-bottom: 20px;">
<el-input v-model="secretValue" :placeholder="inputPlaceholder" :show-password="showPassword" />
<el-input
v-model="secretValue"
:placeholder="inputPlaceholder"
:show-password="showPassword"
@keyup.enter.native="handleConfirm"
/>
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
<el-button
:disabled="smsBtnDisabled"
@@ -116,15 +121,17 @@ export default {
}
},
mounted() {
// const onRecvCallback = _.debounce(this.performConfirm, 500)
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
},
beforeDestroy() {
this.$eventBus.$off('showConfirmDialog', this.performConfirm)
},
methods: {
handleSubTypeChange(val) {
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
this.smsWidth = val === 'sms' ? 6 : 0
},
performConfirm({ response, callback, cancel }) {
performConfirm: _.throttle(function({ response, callback, cancel }) {
if (this.processing || this.visible) {
return
}
@@ -145,6 +152,7 @@ export default {
this.title = this.$t('auth.NeedReLogin')
this.visible = true
})
return
}
this.subTypeChoices = data.content
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
@@ -159,7 +167,7 @@ export default {
}).finally(() => {
this.processing = false
})
},
}, 300),
logout() {
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
},
@@ -195,6 +203,7 @@ export default {
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
this.callback()
this.secretValue = ''
this.visible = false
})
}

View File

@@ -18,9 +18,7 @@ export default {
}
},
data() {
return {
formatterData: ''
}
return {}
},
computed: {
displayValue() {
@@ -69,17 +67,18 @@ export default {
}
},
render(h) {
let formatterData = ''
if (typeof this.formatter === 'function') {
const data = this.formatter(this.item, this.value)
if (data instanceof Promise) {
data.then(res => {
this.formatterData = res
formatterData = res
})
} else {
this.formatterData = data
formatterData = data
}
return (
<span>{this.formatterData}</span>
<span>{formatterData}</span>
)
}
if (this.value instanceof Array) {

View File

@@ -106,16 +106,28 @@ export default {
if (Array.isArray(value)) {
if (typeof value[0] === 'object') {
value.forEach(item => {
const fieldName = `${name}.${item.name}`
if (excludes.includes(fieldName)) {
return
}
this.items.push({
key: item.label,
value: item.value
const firstValue = value[0]
if (firstValue.hasOwnProperty('name')) {
value.forEach(item => {
const fieldName = `${name}.${item.name}`
if (excludes.includes(fieldName)) {
return
}
this.items.push({
key: item.label,
value: item.value
})
})
})
} else {
value.forEach((item, index) => {
const v = Object.entries(item).map(([key, value]) => `${key}:${value}`).join(', ')
const data = { value: v }
if (index === 0) {
data['key'] = label
}
this.items.push(data)
})
}
} else if (typeof value[0] === 'string') {
value.forEach((item, index) => {
let data = {}

View File

@@ -1,7 +1,7 @@
<template>
<IBox :fa="fa" :title="title">
<el-form class="content" label-position="left" label-width="25%">
<el-form-item v-for="item in items" :key="item.key" :label="item.key">
<el-form-item v-for="item in iItems" :key="item.key" :label="item.key">
<ItemValue :value="item.value" class="item-value" v-bind="item" />
</el-form-item>
</el-form>
@@ -35,6 +35,13 @@ export default {
type: String,
default: 'left'
}
},
data() {
return {
iItems: this.items.filter(item => {
return !item.hasOwnProperty('has') || item.has === true
})
}
}
}
</script>

View File

@@ -103,6 +103,10 @@ export default {
type: Function,
default: (obj, that) => {}
},
allowCreate: {
type: Boolean,
default: false
},
onDeleteSuccess: {
type: Function,
default(obj, that) {
@@ -146,6 +150,10 @@ export default {
that.$refs.select2.clearSelected()
that.$message.success(that.$t('common.AddSuccessMsg'))
}
},
getHasObjects: {
type: Function,
default: null // (objectIds) => {}
}
},
data() {
@@ -163,7 +171,8 @@ export default {
options: this.objects,
value: this.value,
disabled: this.disabled,
disabledValues: []
disabledValues: [],
allowCreate: this.allowCreate
}
}
},
@@ -258,9 +267,15 @@ export default {
return
}
this.select2.disabledValues = this.hasObjectsId
const resp = await createSourceIdCache(this.hasObjectsId)
this.params.spm = resp.spm
await this.loadHasObjects()
if (this.getHasObjects) {
this.getHasObjects(this.hasObjectsId).then((data) => {
this.iHasObjects = data
})
} else {
const resp = await createSourceIdCache(this.hasObjectsId)
this.params.spm = resp.spm
await this.loadHasObjects()
}
},
removeObject(obj) {
this.performDelete(obj, this).then(

View File

@@ -75,7 +75,7 @@ export default {
},
computed: {
iWidth() {
return this.$store.getters.isMobile ? '80%' : this.width
return this.$store.getters.isMobile ? '1000px' : this.width
}
},
methods: {
@@ -92,7 +92,7 @@ export default {
<style lang="scss" scoped>
.dialog >>> .el-dialog {
border-radius: 0.3em;
max-width: 1500px;
max-width: min(100vw, 1500px);
.el-icon-circle-check {
display: none;
@@ -119,9 +119,14 @@ export default {
justify-content: flex-end;
}
}
@media (max-width: 900px) {
.dialog >>> .el-dialog {
max-width: calc(100% - 30px);
}
}
.dialog-footer >>> button.el-button {
font-size: 13px;
padding: 10px 20px;
}
</style>

View File

@@ -1,5 +1,4 @@
import Vue from 'vue'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import ObjectSelect2 from '@/components/Form/FormFields/NestedObjectSelect2.vue'
import NestedField from '@/components/Form/AutoDataForm/components/NestedField.vue'
import Switcher from '@/components/Form/FormFields/Switcher.vue'
@@ -8,6 +7,7 @@ import BasicTree from '@/components/Form/FormFields/BasicTree.vue'
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
import { assignIfNot } from '@/utils/common'
import TagInput from '@/components/Form/FormFields/TagInput.vue'
import TransferSelect from '@/components/Form/FormFields/TransferSelect.vue'
export class FormFieldGenerator {
constructor(emit) {
@@ -45,13 +45,14 @@ export class FormFieldGenerator {
break
case 'field':
type = ''
field.component = Select2
field.component = ObjectSelect2
if (fieldRemoteMeta.required) {
field.el.clearable = false
}
if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
field.component = ObjectSelect2
}
field.el.label = field.label
// if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
// field.component = ObjectSelect2
// }
break
case 'string':
type = 'input'
@@ -76,6 +77,7 @@ export class FormFieldGenerator {
break
case 'm2m_related_field':
field.component = ObjectSelect2
field.el.label = field.label
break
case 'nested object':
type = 'nestedField'
@@ -132,6 +134,9 @@ export class FormFieldGenerator {
case 'comment':
field.el.type = 'textarea'
break
case 'users':
field.component = TransferSelect
field.el.label = field.label
}
return field
}

View File

@@ -2,7 +2,7 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')">
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')" class="crontab-panel">
<CrontabMin
ref="cronmin"
:check="checkNumber"
@@ -59,38 +59,38 @@
<td>
<el-input
v-model.trim="contabValueObj.min"
min="0"
max="5"
size="small"
min="0"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.hour"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.day"
size="small"
onkeyup="value=value.replace(/[^\0-9\\-\*\,]/g,'')"
size="mini"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.month"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.week"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
/>
</td>
</tbody>
@@ -100,7 +100,7 @@
<div style="font-size: 13px;">{{ contabValueString }}</div>
</div>
</div>
<CrontabResult :ex="contabValueString" />
<CrontabResult :ex="contabValueString" @crontabDiffChange="crontabDiffChangeHandle" />
<div class="pop_btn">
<el-button
@@ -130,7 +130,7 @@ import CrontabWeek from './components/Crontab-Week.vue'
import CrontabResult from './components/Crontab-Result.vue'
export default {
name: 'Vcrontab',
name: 'VCrontab',
components: {
CrontabMin,
CrontabHour,
@@ -167,7 +167,8 @@ export default {
week: '*'
// year: "",
},
newContabValueString: ''
newContabValueString: '',
crontabDiff: 0
}
},
computed: {
@@ -364,6 +365,12 @@ export default {
},
// 填充表达式
submitFill() {
const crontabDiffMin = this.crontabDiff / 1000 / 60
if (crontabDiffMin > 0 && crontabDiffMin < 10) {
const msg = this.$tc('common.crontabDiffError')
this.$message.error(msg)
return
}
this.$emit('fill', this.contabValueString)
this.hidePopup()
},
@@ -381,13 +388,16 @@ export default {
for (const j in this.contabValueObj) {
this.changeRadio(j, this.contabValueObj[j])
}
},
crontabDiffChangeHandle(diff) {
this.crontabDiff = diff
}
}
}
</script>
<style scoped>
<style lang='scss' scoped>
.pop_btn {
text-align: center;
float: right;
margin-top: 20px;
}
@@ -453,6 +463,12 @@ export default {
overflow-y: auto;
}
.crontab-panel {
> > > .el-input-number {
margin: 0 5px
}
}
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 10px;

View File

@@ -10,15 +10,15 @@
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="31" /> -
<el-input-number v-model="cycle02" :min="0" :max="31" /> {{ this.$t('common.CronTab.day') }}
<el-input-number v-model="cycle01" :max="31" :min="0" size="mini" /> -
<el-input-number v-model="cycle02" :max="31" :min="0" size="mini" /> {{ this.$t('common.CronTab.day') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="31" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
<el-input-number v-model="average02" :max="31" :min="1" size="mini" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
@@ -27,8 +27,8 @@
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
clearable
multiple
style="width:100%"
>

View File

@@ -10,15 +10,15 @@
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.hour') }}
<el-input-number v-model="cycle01" :max="60" :min="0" size="mini" /> -
<el-input-number v-model="cycle02" :max="60" :min="0" size="mini" /> {{ this.$t('common.CronTab.hour') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
<el-input-number v-model="average02" :max="60" :min="1" size="mini" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
@@ -27,8 +27,8 @@
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
clearable
multiple
style="width:100%"
>

View File

@@ -7,18 +7,11 @@
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.min') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
<el-input-number v-model="average02" :max="60" :min="1" size="mini" />
{{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
@@ -27,13 +20,13 @@
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
clearable
multiple
style="width:100%"
size="small"
style="width:100%"
>
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item-1 }}</el-option>
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item - 1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
@@ -158,7 +151,7 @@ export default {
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -10,15 +10,15 @@
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="1" :max="12" /> -
<el-input-number v-model="cycle02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}
<el-input-number v-model="cycle01" :max="12" :min="1" size="mini" /> -
<el-input-number v-model="cycle02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
<el-input-number v-model="average02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
@@ -27,8 +27,8 @@
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
clearable
multiple
style="width:100%"
>

View File

@@ -14,6 +14,7 @@
<script>
import parser from 'cron-parser'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'CrontabResult',
props: {
@@ -51,6 +52,10 @@ export default {
const cur = interval.next().toString()
this.resultList.push(toSafeLocalDateStr(cur))
}
const first = new Date(this.resultList[0])
const second = new Date(this.resultList[1])
const diff = Math.abs(second - first)
this.$emit('crontabDiffChange', diff)
} catch (error) {
this.isShow = false
// debug(error, 'error')

View File

@@ -10,8 +10,8 @@
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.cycleFromWeek') }}
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
<el-input-number v-model="cycle02" :min="1" :max="7" />
<el-input-number v-model="cycle01" :max="7" :min="1" size="mini" /> -
<el-input-number v-model="cycle02" :max="7" :min="1" size="mini" />
</el-radio>
</el-form-item>
@@ -20,8 +20,8 @@
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
clearable
multiple
style="width:100%"
>

View File

@@ -1,13 +1,19 @@
<template>
<div>
<div class="box">
<el-input v-model="input" clearable @focus="showDialog" @clear="onClear" />
<el-input v-model="input" clearable @clear="onClear" @focus="showDialog" />
</div>
<el-dialog :title="$tc('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
<el-dialog
:title="$tc('common.CronTab.newCron')"
:visible.sync="showCron"
append-to-body
top="8vh"
width="650px"
>
<Crontab
:expression="expression"
@hide="showCron = false"
@fill="crontabFill"
@hide="showCron = false"
/>
</el-dialog>
</div>

View File

@@ -7,13 +7,11 @@
v-bind="data.attrs"
>
<template v-if="data.helpTips" #label>
<el-tooltip placement="bottom" effect="light" popper-class="help-tips">
<div slot="content" v-html="data.helpTips" />
<el-button style="padding: 0">
<i class="fa fa-info-circle" />
</el-button>
</el-tooltip>
{{ data.label }}
<el-tooltip placement="top" effect="light" popper-class="help-tips">
<div slot="content" v-html="data.helpTips" />
<i class="fa fa-question-circle-o" />
</el-tooltip>
</template>
<template v-if="readonly && hasReadonlyContent">
<div
@@ -70,7 +68,8 @@
:key="opt.label"
v-bind="opt"
:label="'value' in opt ? opt.value : opt.label"
>{{ opt.label }}</el-radio>
>{{ opt.label }}
</el-radio>
</template>
</custom-component>
<div v-if="data.helpText" class="help-block" v-html="data.helpText" />

View File

@@ -53,7 +53,6 @@ export default {
if (!this.value || this.value.length === 0) {
return
}
console.log('Value: ', this.value)
if (this.value.indexOf('all') > -1) {
this.type = 'all'
} else {

View File

@@ -2,7 +2,7 @@
<div class="code-editor" style="font-size: 12px">
<div class="toolbar">
<div
v-for="(item,index) in toolbar.left"
v-for="(item,index) in iActions"
:key="index"
style="display: inline-block; margin: 0 2px"
>
@@ -93,6 +93,16 @@
</el-tooltip>
</div>
<div v-if="toolbar.hasOwnProperty('fold')" class="fold">
<el-tooltip :content="$tc('common.MoreActions')" placement="top">
<i
class="fa"
:class="[isFold ? 'fa-angle-double-right': 'fa-angle-double-down']"
@click="onChangeFold"
/>
</el-tooltip>
</div>
<div class="right-side" style="float: right">
<div
v-for="(item,index) in toolbar.right"
@@ -154,9 +164,19 @@ export default {
}
},
data() {
return {}
return {
isFold: true
}
},
computed: {
iActions() {
let actions = this.toolbar.left || {}
const fold = this.toolbar.fold || {}
if (!this.isFold) {
actions = { ...actions, ...fold }
}
return actions
},
iValue: {
get() {
return this.value
@@ -179,6 +199,9 @@ export default {
}
},
methods: {
onChangeFold() {
this.isFold = !this.isFold
},
getLabel(value, items) {
for (const item of items) {
if (item.value === value) {
@@ -205,6 +228,16 @@ export default {
margin-bottom: 5px;
}
.fold {
display: inline-block;
padding-left: 4px;
i {
font-weight: bold;
font-size: 15px;
cursor: pointer;
}
}
> > > .CodeMirror pre.CodeMirror-line,
> > > .CodeMirror-linenumber.CodeMirror-gutter-elt {
line-height: 18px !important;

View File

@@ -70,6 +70,11 @@ export default {
id: 'symbol',
label: this.$t('common.SpecialSymbol'),
type: 'switch'
},
{
id: 'exclude_symbols',
label: this.$t('common.ExcludeSymbol'),
type: 'input'
}
]
}

View File

@@ -24,6 +24,7 @@
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'PhoneInput',
@@ -35,21 +36,7 @@ export default {
},
data() {
return {
rawValue: {},
countries: [
{ name: 'China(中国)', value: '+86' },
{ name: 'HongKong(中国香港)', value: '+852' },
{ name: 'Macao(中国澳门)', value: '+853' },
{ name: 'Taiwan(中国台湾)', value: '+886' },
{ name: 'America(America)', value: '+1' },
{ name: 'Russia(Россия)', value: '+7' },
{ name: 'France(français)', value: '+33' },
{ name: 'Britain(Britain)', value: '+44' },
{ name: 'Germany(Deutschland)', value: '+49' },
{ name: 'Japan(日本)', value: '+81' },
{ name: 'Korea(한국)', value: '+82' },
{ name: 'India(भारत)', value: '+91' }
]
rawValue: {}
}
},
computed: {
@@ -58,7 +45,13 @@ export default {
return ''
}
return `${this.rawValue.code}${this.rawValue.phone}`
}
},
countries: {
get() {
return this.publicSettings.COUNTRY_CALLING_CODES
}
},
...mapGetters(['publicSettings'])
},
mounted() {
this.rawValue = this.value || { code: '+86', phone: '' }

View File

@@ -8,7 +8,7 @@
:title="$tc('assets.PlatformProtocolConfig') + '' + protocol.name"
class="setting-dialog"
v-bind="$attrs"
width="70%"
width="800px"
v-on="$listeners"
>
<el-alert v-if="disabled && platformDetail" style="margin-bottom: 10px" type="success">
@@ -83,6 +83,14 @@ export default {
hidden: (formValue) => formValue['autofill'] !== 'script'
}
}
},
public: {
disabled: this.protocol.name === 'winrm',
hidden: (formValue) => {
if (this.protocol.name === 'winrm') {
formValue['public'] = false
}
}
}
}
}

View File

@@ -172,7 +172,11 @@ export default {
return params
}
const defaultTransformOption = (item) => {
return { label: item.name, value: item.id }
if (typeof item === 'object') {
return { label: item.name, value: item.id }
} else {
return { label: item, value: item }
}
}
const transformOption = this.ajax.transformOption || defaultTransformOption
const defaultFilterOption = (item) => {
@@ -207,6 +211,12 @@ export default {
}
},
watch: {
disabled(newValue, oldValue) {
this.selectDisabled = newValue
},
options(newValue, oldValue) {
this.iOptions = newValue
},
iAjax(newValue, oldValue) {
this.$log.debug('Select url changed: ', oldValue, ' => ', newValue)
this.refresh()
@@ -215,13 +225,6 @@ export default {
handler(newValue, oldValue) {
},
deep: true
},
iOptions(val) {
if (val.length === 0) {
this.remote = false
} else {
this.remote = true
}
}
},
async mounted() {
@@ -231,6 +234,7 @@ export default {
this.$log.debug('Value is : ', this.value)
this.iValue = this.value
this.initialized = true
this.$emit('initialized', true)
}, 100)
}
this.$nextTick(() => {
@@ -352,7 +356,7 @@ export default {
})
},
clearSelected() {
this.iValue = []
this.iValue = this.multiple ? [] : ''
},
checkDisabled(item) {
return item.disabled === undefined ? this.disabledValues.indexOf(item.value) !== -1 : item.disabled
@@ -368,6 +372,7 @@ export default {
this.refresh()
this.$log.debug('Visible change, refresh select2')
}
this.$emit('visible-change', visible)
}
}
}

View File

@@ -21,8 +21,8 @@
:type="inputType"
class="search-input"
@blur="focus = false"
@change="handleConfirm"
@focus="focus = true"
@change="handleChange"
@select="handleSelect"
@keyup.enter.native="handleConfirm"
/>
@@ -99,6 +99,9 @@ export default {
this.filterValue = item.value
this.handleConfirm()
},
handleChange: _.debounce(function(item) {
this.handleConfirm()
}, 240),
handleConfirm() {
if (this.filterValue === '') return

View File

@@ -0,0 +1,172 @@
<template>
<div>
<Select2
ref="select2"
v-model="iValue"
v-bind="select2"
@initialized="handleSelectInitialed"
@input="onInputChange"
v-on="$listeners"
@focus.stop.prevent="handleFocus"
/>
<Dialog
v-if="showTransfer"
:close-on-click-modal="false"
:title="label"
:visible.sync="showTransfer"
class="the-dialog"
width="730px"
@cancel="handleTransCancel"
@confirm="handleTransConfirm"
>
<krryPaging v-if="selectInitialized" ref="pageTransfer" class="transfer" v-bind="pagingTransfer" />
</Dialog>
</div>
</template>
<script>
import Select2 from '@/components/Form/FormFields/Select2.vue'
import krryPaging from '@/components/Libs/Krry/paging/index.vue'
import Dialog from '@/components/Dialog/index.vue'
export default {
name: 'TransferSelect',
components: { krryPaging, Select2, Dialog },
props: {
value: {
type: Array,
default: () => []
},
url: {
type: String,
default: ''
},
transformOption: {
type: Function,
default: null
},
ajax: {
type: Object,
default: () => {
return {}
}
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
}
},
data() {
const vm = this
const transformOption = vm.transformOption || vm.ajax.transformOption || ((item) => {
return { label: item.name, value: item.id }
})
const url = vm.url || vm.ajax.url
const getPageData = async({ pageIndex, pageSize, keyword }) => {
const limit = pageSize
const offset = (pageIndex - 1) * pageSize
const params = {
'limit': limit,
'offset': offset,
'fields_size': 'mini'
}
if (keyword) {
params['search'] = keyword
}
const data = await this.$axios.get(url, { params })
return data['results'].map(item => {
const n = transformOption(item)
return { id: n.value, label: n.label }
})
}
return {
showTransfer: false,
selectInitialized: false,
select2: {
options: [],
multiple: true,
disabled: this.disabled,
ajax: {
url: url,
transformOption: transformOption
}
},
transferLoading: false,
pagingTransfer: {
pageSize: 100,
filterable: true,
async: true,
dataList: [],
getPageData: function(pageIndex, pageSize) {
return getPageData({ pageIndex, pageSize })
},
getSearchData: async function(keyword, pageIndex, pageSize) {
return getPageData({ keyword, pageIndex, pageSize })
},
selectedData: [],
showClearBtn: true
}
}
},
computed: {
iValue: {
get() {
let value = this.value
if (!value || value.length === 0) {
return []
}
if (typeof value[0] === 'object') {
value = value.map(item => {
return item.id
})
}
return _.uniq(value)
},
set(val) {
this.emit(val)
}
}
},
methods: {
emit(val) {
const value = _.uniq(val)
this.$emit('input', value)
},
onInputChange(val) {
this.emit(val)
},
handleFocus() {
this.$refs.select2.selectRef.blur()
this.pagingTransfer.selectedData = this.$refs.select2.iOptions.map(item => {
return { id: item.value, label: item.label }
}).filter(item => {
return this.iValue.includes(item.id)
})
this.showTransfer = true
},
handleSelectInitialed() {
this.selectInitialized = true
},
handleTransCancel() {
this.showTransfer = false
},
handleTransConfirm() {
const selectedData = this.$refs.pageTransfer.selectListCheck
const options = selectedData.map(item => {
return { value: item.id, label: item.label }
})
this.select2.options = options
this.emit(options.map(item => item.value))
this.showTransfer = false
}
}
}
</script>
<style scoped>
</style>

View File

@@ -8,7 +8,7 @@
<div v-if="tip !== ''" class="help-block">{{ tip }}</div>
<input v-model="value" hidden type="text" v-on="$listeners">
<div>
<img :src="preview" v-bind="$attrs">
<img :class="showBG ? 'show-bg' : ''" :src="preview" v-bind="$attrs">
</div>
</div>
</template>
@@ -27,6 +27,10 @@ export default {
accept: {
type: String,
default: '*'
},
showBG: {
type: Boolean,
default: false
}
},
data() {
@@ -74,6 +78,8 @@ export default {
}
</script>
<style scoped>
<style lang="scss" scoped>
.show-bg {
background-color: var(--banner-bg);
}
</style>

View File

@@ -204,7 +204,12 @@ export default {
},
formatWeektime(col) {
const timeStamp = 1542384000000 // '2018-11-17 00:00:00'
const beginStamp = timeStamp + col * 1800000 // col * 30 * 60 * 1000
const timezone = 8
const offsetGMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟
const nowDate = new Date(timeStamp).getTime()
const targetStamp = new Date(nowDate + offsetGMT * 60 * 1000 + timezone * 60 * 60 * 1000).getTime()
const beginStamp = targetStamp + col * 1800000 // col * 30 * 60 * 1000
const endStamp = beginStamp + 1800000
const begin = this.formatDate(new Date(beginStamp), 'hh:mm')

View File

@@ -0,0 +1,6 @@
import krryCascader from './index.vue'
// 组件的 name 作为组件调用名
krryCascader.install = Vue => Vue.component(krryCascader.name, krryCascader)
export default krryCascader

View File

@@ -0,0 +1,75 @@
<template>
<div class="krry-main">
<!-- 地区 -->
<krry-container
:box-operation="boxOperation"
:box-title="boxTitle"
:data-obj="dataObj"
:filter-placeholder="filterPlaceholder"
:filterable="filterable"
:on-change-selected="emitChangeSelected"
:selected-data="selectedData"
/>
</div>
</template>
<script>
import krryContainer from './models/container'
export default {
name: 'KrCascader',
components: {
krryContainer
},
props: {
boxTitle: {
type: Array,
default: () => ['省份', '城市', '区县', '选中地域']
},
boxOperation: {
type: Array,
default: () => ['添加省份', '添加城市', '添加区县', '删除地域']
},
dataObj: {
type: Object,
default: () => {}
},
selectedData: {
type: Array,
default: () => []
},
filterable: {
type: Boolean,
default: () => false
},
filterPlaceholder: {
type: String,
default: () => '请输入搜索内容'
}
},
data() {
return {
hasSelectData: []
}
},
watch: {},
created() {},
methods: {
// 获取已选数据的监听事件
emitChangeSelected(val) {
this.hasSelectData = val
this.$emit('onChange', val)
},
// 提供获取已选数据的钩子
getSelectedData() {
return this.hasSelectData
}
}
}
</script>
<style lang="scss" scoped>
.krry-main {
min-width: 906px;
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<div>
<krry-box
ref="prov"
:operation="boxOperation[0]"
:title="boxTitle[0]"
:operate-id="0"
:district-list="provinceList"
:filterable="filterable"
:filter-placeholder="filterPlaceholder"
@check-district="checkProvince"
@selected-checked="selectedProvince"
/>
<krry-box
ref="city"
:operation="boxOperation[1]"
:title="boxTitle[1]"
:operate-id="1"
:district-list="cityList"
:filterable="filterable"
:filter-placeholder="filterPlaceholder"
@check-district="checkCity"
@selected-checked="selectedCity"
/>
<krry-box
ref="county"
:operation="boxOperation[2]"
:title="boxTitle[2]"
:operate-id="2"
:district-list="countyList"
:filterable="filterable"
:filter-placeholder="filterPlaceholder"
@selected-checked="selectedCountry"
/>
<span class="inner-center el-icon-d-arrow-right" />
<krry-box
style="width: 260px"
:operation="boxOperation[3]"
:title="boxTitle[3]"
:district-list="checkedDistrict"
:filterable="filterable"
:filter-placeholder="filterPlaceholder"
@delete-checked="deleteCheck"
/>
</div>
</template>
<script>
import krryBox from './models/box'
export default {
components: {
krryBox
},
props: {
boxTitle: {
type: Array,
default: () => []
},
boxOperation: {
type: Array,
default: () => []
},
// 地域数据
dataObj: {
type: Object,
default: () => {}
},
// 已选数据
selectedData: {
type: Array,
default: () => []
},
onChangeSelected: {
type: Function,
default: () => () => {}
},
filterable: {
type: Boolean,
default: () => false
},
filterPlaceholder: {
type: String,
default: () => ''
}
},
data() {
return {
flag: false, // 分仓对应的省id变量的监听器的锁第一次触发不执行数据还未初始化
provinceList: [], // 省级数据
cityList: [], // 市级数据
countyList: [], // 区县级数据
checkedDistrict: [], // 已选数据
filterProvince: [], // 省级过滤id
filterCity: [], // 市级过滤id
filterCounty: [] // 区县级过滤id
}
},
computed: {
// 映射出选中区域的数据Id
selectDistrictId() {
return this.checkedDistrict.map((val) => val.id)
}
},
watch: {
dataObj: {
handler() {
this.getDistrict()
},
deep: true
},
selectedData: {
handler() {
this.getDistrict()
},
deep: true
},
checkedDistrict(newVal) {
this.onChangeSelected(newVal)
}
},
created() {
this.getDistrict()
},
methods: {
// 获取区域数据
async getDistrict() {
// 从后台传回经过处理的数据
this.flag = true // 数据加载完成,解锁
// 执行已选数据的过滤
this.checkedDistrict = JSON.parse(JSON.stringify(this.selectedData))
this.initFilter(this.checkedDistrict)
// 获取省级数据
this.getProvince()
},
// 获取省级数据
getProvince() {
this.provinceList = [] // 首先清空
for (const key in this.dataObj.province) {
this.provinceList.push({
id: key,
label: this.dataObj.province[key]
})
// 省级过滤处理
this.handleFilterProvince()
}
},
// 获取市级数据子组件自定义的穿梭框传回的数据val[区域obj, 区域obj,...]
checkProvince(val) {
const obj = val[val.length - 1]
let flag = true
if (obj !== undefined) {
const id = obj.id
for (const key in this.dataObj.city) {
if (id === key) {
// 匹配到的id将对应的市级数据传递到子组件
this.cityList = this.dataObj.city[key]
// 过滤处理
this.handleFilterCity()
// 过滤处理
// 再清空上一次的县级数据
this.countyList = []
// 将父级对象放进市级组件
this.$refs.city.father = {
id: id,
label: obj.label
}
flag = false
break
}
}
}
// 如果市级没有匹配到,市级和区级都显示为空
if (flag) {
this.cityList = []
this.countyList = []
}
},
// 获取县级数据子组件自定义的穿梭框传回的数据val[区域obj, 区域obj,...]
checkCity(val) {
const obj = val[val.length - 1]
let flag = true
if (obj !== undefined) {
const id = obj.id
for (const key in this.dataObj.county) {
if (id.toString() === key) {
// 匹配到的id将对应的区级数据传递到子组件
this.countyList = this.dataObj.county[key]
// 过滤处理
this.handleFilterCounty()
// 获取省级的数据
const fatherId = this.$refs.city.father.id
const fatherText = this.$refs.city.father.label
// 拼接上市级数据放进县级组件
this.$refs.county.father = {
id: fatherId + '-' + id,
label: fatherText + '-' + obj.label
}
flag = false
break
}
}
}
// 区级没有匹配到,显示为空
if (flag) {
this.countyList = []
}
},
// 从省级添加到已选区域参数val省级对象数组filterId所选择的省级id数组
selectedProvince(val, filterId) {
this.checkedDistrict = this.checkedDistrict.concat(val)
this.filterProvince = this.filterProvince.concat(filterId)
// 如果过滤的市级区域,还有县级区域,合并成一个市级
for (const val of filterId) {
for (const vq of this.checkedDistrict) {
const selectId = vq.id.split('-')
// 拆分的数组长度大于1说明有市级以下的区域合并成一个省级区域
if (selectId.length > 1 && selectId[0] === val) {
// 在已选择的区域中删除市级数据,合并成一个省级
this.checkedDistrict = this.checkedDistrict.filter(
(vl) => vl !== vq
)
// 当前省级已被合并,从过滤数组中删除该市级和县级数据
this.filterCity = this.filterCity.filter(
(vf) => vf.toString() !== selectId[1]
)
this.filterCounty = this.filterCounty.filter(
(vs) => vs.toString() !== selectId[2]
)
}
}
}
// 清空下面的市级和县级区域
this.cityList = []
this.countyList = []
// 过滤处理
this.handleFilterProvince()
},
// 从市级添加到已选区域
selectedCity(val, filterId) {
this.checkedDistrict = this.checkedDistrict.concat(val)
this.filterCity = this.filterCity.concat(filterId)
// 如果过滤的市级区域,还有县级区域,合并成一个市级
for (const val of filterId) {
for (const vq of this.checkedDistrict) {
const selectId = vq.id.split('-')
// 拆分的数组长度为3说明有县级区域并且该市级区域与当前加入市级区域的id相同合并成一个市级区域
if (selectId.length === 3 && selectId[1] === val.toString()) {
// 在已选择的区域中删除县级数据,合并成一个市级
this.checkedDistrict = this.checkedDistrict.filter(
(vl) => vl !== vq
)
// 当前市级已被合并,从过滤数组中删除该县级数据
this.filterCounty = this.filterCounty.filter(
(vs) => vs.toString() !== selectId[2]
)
}
}
}
// 清空下面的县级区域
this.countyList = []
// 过滤处理
this.handleFilterCity()
},
// 从县级添加到已选区域
selectedCountry(val, filterId) {
this.checkedDistrict = this.checkedDistrict.concat(val)
this.filterCounty = this.filterCounty.concat(filterId)
// 过滤处理
this.handleFilterCounty()
},
// 省级过滤处理
handleFilterProvince() {
let newPro = Array.from(this.provinceList)
for (const val of this.filterProvince) {
newPro = newPro.filter((vq) => String(vq.id) !== String(val))
}
this.provinceList = Array.from(newPro)
},
// 市级过滤处理
handleFilterCity() {
let newCity = Array.from(this.cityList)
for (const val of this.filterCity) {
newCity = newCity.filter((vq) => String(vq.id) !== String(val))
}
this.cityList = Array.from(newCity)
},
// 区县级过滤处理
handleFilterCounty() {
let newCounty = Array.from(this.countyList)
for (const val of this.filterCounty) {
newCounty = newCounty.filter((vq) => String(vq.id) !== String(val))
}
this.countyList = Array.from(newCounty)
},
// 删除已选区域参数deleteVal要删除的区域对象数组
deleteCheck(deleteVal) {
for (const val of deleteVal) {
const selectId = val.id.split('-')
const length = selectId.length
switch (length) {
case 1: {
// 长度只有1只有省级数据删除对应省级的filter中的数据
this.filterProvince = this.filterProvince.filter(
(vs) => vs !== selectId[0]
)
// 重新获取县级数据
this.getProvince()
break
}
case 2: {
// 长度为2到达市级数据删除对应市级的filter中的数据
this.filterCity = this.filterCity.filter(
(vs) => vs.toString() !== selectId[1]
)
// 重新获取市级数据
if (this.$refs.prov.selectedDistrict.length) {
// 省级已勾选才显示区级
this.checkProvince([this.$refs.city.father])
}
break
}
case 3: {
// 长度为3到达县级数据删除对应县级的filter中的数据
this.filterCounty = this.filterCounty.filter(
(vs) => vs.toString() !== selectId[2]
)
if (this.$refs.city.selectedDistrict.length) {
// 市级已勾选才显示区级
const fatherId = this.$refs.county.father.id.split('-')[1]
const fatherText = this.$refs.county.father.label.split('-')[1]
const obj = [{ id: fatherId, label: fatherText }]
// 重新获取县级数据参数当前市级ID的对象数组obj:[{id:id,label:label}]
this.checkCity(obj)
}
break
}
}
// 刷新已选区域
this.checkedDistrict = this.checkedDistrict.filter(
(vd) => vd.id !== val.id
)
}
},
// 初始化过滤器 参数addVal要增加的区域对象数组
initFilter(addVal) {
for (const val of addVal) {
const selectId = val.id.split('-')
const length = selectId.length
switch (length) {
case 1:
this.filterProvince.push(selectId[0])
break
case 2:
this.filterCity.push(selectId[1])
break
case 3:
this.filterCounty.push(selectId[2])
break
}
}
}
}
}
</script>
<style lang='scss' scoped>
.inner-center {
margin: 0 5px;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<div class="el-transfer-panel district-panel">
<div class="el-transfer-panel__header">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>{{ title }}</el-checkbox>
<span
class="check-number"
>{{ selectedDistrict.length }}/{{ districtListMock.length }}</span>
</div>
<div class="el-transfer-panel__body">
<div
v-if="filterable"
class="el-transfer-panel__filter el-input el-input--small el-input--prefix"
>
<input
v-model="searchWord"
type="text"
autocomplete="off"
:placeholder="filterPlaceholder"
class="el-input__inner"
>
<span class="el-input__prefix" style="left: 0px">
<i class="el-input__icon el-icon-search" />
</span>
</div>
<el-checkbox-group
v-if="districtListMock.length > 0"
v-model="selectedDistrict"
:class="{ expand: !filterable }"
@change="handleCheckedChange"
>
<el-checkbox
v-for="(city, index) in districtListMock"
:key="index"
class="el-transfer-panel__item"
:disabled="city.disabled"
:title="city.label"
:label="city"
>{{ city.label }}</el-checkbox>
</el-checkbox-group>
<p v-else class="no-data">无数据</p>
</div>
<div class="vip-footer">
<el-button
type="text"
:disabled="selectedDistrict.length<=0"
size="small"
round
@click="checkedSelected"
>
<span>{{ operation }}</span>
</el-button>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
title: {
type: String,
default: () => ''
},
operation: {
type: String,
default: () => ''
},
operateId: {
type: Number,
default: () => 0
},
// 区域数据
districtList: {
type: Array,
default: () => []
},
filterable: {
type: Boolean,
default: () => false
},
filterPlaceholder: {
type: String,
default: () => ''
}
},
data() {
return {
districtListMock: [], // 展示的数据 (搜索会自动修改这个数组)
selectedDistrict: [], // 已选择,数据格式:[区域id,id,id...]
father: {}, // 父级数据
isIndeterminate: false,
checkAll: false,
searchWord: '',
buttonAble: true
}
},
watch: {
// 搜索框的监听器
searchWord(newWord, oldWord) {
// 重新获取数据
this.districtListMock = this.districtList
// 过滤掉数据,保留搜索的数据
this.districtListMock = this.districtListMock.filter((val) =>
val.label.includes(newWord)
)
},
// 当点击省级或市级,自动监听并更新市级或区级的列表
districtList() {
this.getDistrict()
// 如果区域数据为空,则已选择的数据也要清空
if (this.districtList.length === 0) {
this.selectedDistrict = []
}
},
// districtListMock 和 checkAll 的监听器
districtListMock() {
// 当方框中无已选择的数据时不能勾选checkBox
if (this.selectedDistrict.length === 0) {
this.checkAll = false
this.isIndeterminate = false
}
},
// 当列表中无数据时不能勾选checkBox
checkAll() {
this.checkAll = this.districtListMock.length === 0 ? false : this.checkAll
}
},
created() {
this.getDistrict()
},
methods: {
// 获取区域数据
getDistrict() {
this.districtListMock = this.districtList
// 已选择的清空
this.selectedDistrict = []
},
// 单选
handleCheckedChange(value) {
const checkedCount = value.length
this.checkAll = checkedCount === this.districtListMock.length
this.isIndeterminate =
checkedCount > 0 && checkedCount < this.districtListMock.length
this.$emit('check-district', value)
},
// 全选
handleCheckAllChange(val) {
this.selectedDistrict = val ? this.districtListMock.filter(val => !val.disabled).map((val) => val) : []
this.isIndeterminate = false
},
// 添加至已选 或 删除已选区域
checkedSelected() {
const selectedList = []
const filterId = []
if (this.operateId === 0) {
// 省级添加
for (const val of this.selectedDistrict) {
selectedList.push({
id: val.id,
label: val.label
})
filterId.push(val.id)
}
this.$emit('selected-checked', selectedList, filterId)
} else if (this.operateId === 1 || this.operateId === 2) {
// 市级或县级添加
for (const val of this.selectedDistrict) {
selectedList.push({
id: this.father.id + '-' + val.id,
label: this.father.label + '-' + val.label
})
filterId.push(val.id)
}
this.$emit('selected-checked', selectedList, filterId)
} else {
// 删除已选区域
for (const val of this.selectedDistrict) {
selectedList.push({
id: val.id,
label: val.label
})
}
this.$emit('delete-checked', selectedList)
}
}
}
}
</script>
<style lang="scss" scoped>
.district-panel {
width: 200px;
.el-transfer-panel__header {
.el-checkbox {
display: inline-block;
}
}
.el-transfer-panel__body {
height: 292px;
padding: 6px 0;
.el-transfer-panel__filter {
line-height: 0;
margin: 6px 14px 12px;
}
}
.el-checkbox-group {
height: 240px;
overflow: auto;
&.expand {
height: 290px;
}
.el-transfer-panel__item {
display: block;
}
}
.check-number {
position: absolute;
right: 15px;
top: 0;
color: #909399;
font-size: 12px;
font-weight: 400;
}
.no-data {
font-size: 14px;
margin: 0;
height: 30px;
line-height: 30px;
padding: 6px 15px 0;
color: #909399;
text-align: center;
}
.vip-footer {
position: relative;
margin: 0;
padding: 5px 0;
text-align: center;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@@ -0,0 +1,443 @@
<template>
<div class="krry-main">
<el-row :gutter="10">
<el-col :md="10" :sm="24">
<krry-box
ref="noSelect"
:async="async"
:async-search-flag="asyncSearchFlag"
:data-show-list="notSelectDataList"
:filter-placeholder="filterPlaceholder[0] || $tc('common.Search')"
:filterable="filterable"
:highlight-color="highlightColor"
:is-highlight="isHighlight"
:is-last-page="isLastPage"
:operate-id="0"
:page-size="pageSize"
:page-texts="pageTexts"
:show-clear-btn="showClearBtn"
:title="boxTitle[0] || $tc('common.Selection')"
@check-district="noCheckSelect"
@search-word="searchWord"
@check-disable="checkDisable"
@get-data="getData"
@get-data-by-keyword="getDataByKeyword"
@clear-input="clearQueryInp('left')"
/>
</el-col>
<el-col :md="4" :sm="24" class="buttons">
<div class="opera">
<el-button
:disabled="disablePre"
class="el-transfer__button"
icon="el-icon-arrow-left"
size="mini"
@click="deleteData"
/>
<el-button
:disabled="disableNex"
class="el-transfer__button"
icon="el-icon-arrow-right"
size="mini"
type="primary"
@click="addData"
/>
</div>
</el-col>
<el-col :md="10" :sm="24">
<krry-box
ref="hasSelect"
:data-show-list="checkedData"
:filter-placeholder="filterPlaceholder[1] || $tc('common.Search')"
:filterable="filterable"
:highlight-color="highlightColor"
:is-highlight="isHighlight"
:operate-id="1"
:page-size="pageSize"
:page-texts="pageTexts"
:show-clear-btn="showClearBtn"
:title="boxTitle[1] || $tc('common.Selected')"
@check-district="hasCheckSelect"
@search-word="searchWord"
@check-disable="checkDisable"
@clear-input="clearQueryInp('right')"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import krryBox from './models/box'
export default {
name: 'KrryPaging',
components: {
krryBox
},
props: {
boxTitle: {
type: Array,
// default: () => [this.$tc('common.Selection'), this.$tc('common.Selected')]
default: () => ['', '']
},
pageSize: {
type: Number,
default: 160
},
dataList: {
type: Array,
default: () => []
},
selectedData: {
type: Array,
default: () => []
},
filterable: {
type: Boolean,
default: () => false
},
filterPlaceholder: {
type: Array,
default: () => ['', '']
// default: () => [this.$tc('common.Search'), this.$tc('common.Search')]
},
pageTexts: {
type: Array,
default: () => ['', '']
// default: () => ['< ' + this.$tc('common.PagePrev'), this.$tc('common.PageNext') + ' >']
},
sort: {
type: Boolean,
default: () => false
},
async: {
type: Boolean,
default: () => false
},
getPageData: {
type: Function,
default: () => []
},
getSearchData: {
type: Function,
default: () => []
},
isHighlight: {
type: Boolean,
default: () => false
},
highlightColor: {
type: String,
default: () => '#ff2b2b'
},
showClearBtn: {
type: Boolean,
default: () => false
}
},
data() {
return {
notSelectDataList: [], // 未选中(已过滤出已选)的数据
checkedData: [], // 已选中的数据
dataListNoCheck: [], // 未搜索的数据
selectListCheck: [], // 未搜索的数据
noCheckData: [], // 未选中区域的已勾选的数据(待添加到已选区域)
hasCheckData: [], // 已选中区域的已勾选的数据(从未选区域中待删除)
noSelectKeyword: '',
haSelectKeyword: '',
disablePre: true,
disableNex: true,
manualEmpty: false, // 是否手动将已选区数据置为空
asyncDataList: [], // 异步请求的数据源
isLastPage: false // 异步请求是否是最后一页
}
},
computed: {
// 传递到后台保存的数据(已选中的数据的 id 数组)
selectIdList() {
return this.selectListCheck.map(item => item.id)
},
originList() {
return this.async ? this.asyncDataList : this.dataList
},
asyncSearchFlag() {
// 是否设置了异步搜索方法
return this.async && this.getSearchData !== undefined
}
},
watch: {
selectIdList(newVal) {
// 获取已选数据的监听事件
const moveKeys = [
...this.noCheckData.map(item => item.id),
...this.hasCheckData.map(item => item.id)
]
this.hasCheckData = []
this.noCheckData = []
this.$emit('onChange', newVal, moveKeys)
},
dataList: {
handler() {
!this.async && this.initData()
},
deep: true
},
selectedData: {
handler() {
this.initData(true)
},
deep: true
}
},
created() {
this.async ? this.getData(1, true) : this.initData(true)
},
methods: {
// 分页数据,初始化数据,过滤已选数据
initData(selectedChange) {
// this.checkedData 为空 且 从来没有将已选区置为空,则从 selectedData 获取
if ((!this.checkedData.length && !this.manualEmpty) || selectedChange) {
this.checkedData = JSON.parse(JSON.stringify(this.selectedData))
const keywords = this.$refs.hasSelect
? this.$refs.hasSelect.searchWord
: ''
keywords && this.searchWord(keywords, 1)
}
if (!this.async) {
this.selectListCheck = JSON.parse(JSON.stringify(this.checkedData))
const checkDataId = this.selectListCheck.map(ele => ele.id)
this.notSelectDataList = this.originList.filter(
ele => !checkDataId.includes(ele.id)
)
this.dataListNoCheck = JSON.parse(
JSON.stringify(this.notSelectDataList)
)
} else {
if (selectedChange) {
this.selectListCheck = JSON.parse(JSON.stringify(this.checkedData))
}
const checkDataId = this.selectListCheck.map(ele => ele.id)
this.notSelectDataList = this.originList.filter(
ele =>
!checkDataId.includes(ele.id) &&
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
)
this.dataListNoCheck = this.originList.filter(
ele => !checkDataId.includes(ele.id)
)
}
},
searchWord(keyword, titleId) {
// 过滤掉数据,保留搜索的数据
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
if (titleId === 0) {
this.noSelectKeyword = keyword
if (!this.asyncSearchFlag) {
this.notSelectDataList = this.dataListNoCheck.filter(val =>
val.label.includes(keyword)
)
}
} else {
this.haSelectKeyword = keyword
this.checkedData = this.selectListCheck.filter(val =>
val.label.includes(keyword)
)
}
const refsName = titleId === 0 ? 'noSelect' : 'hasSelect'
// 延迟执行
setTimeout(() => {
!this.async && this.$refs[refsName].initData()
}, 0)
},
// 检查左右按钮可用性
checkDisable(data, operateId) {
if (operateId === 0) {
this.disableNex = !(data.length > 0)
} else {
this.disablePre = !(data.length > 0)
}
},
// 未选中区域的选泽
noCheckSelect(val) {
this.noCheckData = val
},
// 已选中区域的选泽
hasCheckSelect(val) {
this.hasCheckData = val
},
// 关键把未选择的数据当做已选择的过滤数组把已选择的数据当做未选择的过滤数组在全局data进行过滤最后进行一次搜索
// 添加至已选
addData() {
const noCheckDataId = this.noCheckData.map(ele => ele.id)
// 待选区数据过滤
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
this.notSelectDataList = this.notSelectDataList.filter(
ele =>
!noCheckDataId.includes(ele.id) &&
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
)
this.dataListNoCheck = this.dataListNoCheck.filter(
ele => !noCheckDataId.includes(ele.id)
)
// 已选区数据增加
if (!this.async && this.sort) {
// 排序,从固定不变的所有数据中过滤,顺序就不会乱。但若数据量大就会比较卡
// 异步分页不支持排序
const dataListNoCheckId = this.dataListNoCheck.map(ele => ele.id)
this.checkedData = this.originList.filter(
ele =>
!dataListNoCheckId.includes(ele.id) &&
ele.label.includes(this.haSelectKeyword)
)
this.selectListCheck = this.originList.filter(
ele => !dataListNoCheckId.includes(ele.id)
)
} else {
// 这种效率更高的方法,但不能排序
this.checkedData.push(...this.noCheckData)
this.selectListCheck.push(...this.noCheckData)
this.checkedData = this.checkedData.filter(ele =>
ele.label.includes(this.haSelectKeyword)
)
}
},
// 从已选中删除
deleteData() {
// 已选区数据过滤
const hasCheckDataId = this.hasCheckData.map(ele => ele.id)
this.checkedData = this.checkedData.filter(
ele =>
!hasCheckDataId.includes(ele.id) &&
ele.label.includes(this.haSelectKeyword)
)
this.selectListCheck = this.selectListCheck.filter(
ele => !hasCheckDataId.includes(ele.id)
)
this.manualEmpty = !this.checkedData.length
// 待选区数据增加
const selectListCheckId = this.selectListCheck.map(ele => ele.id)
// const checkedDataId = this.checkedData.map(ele => ele.id)
// 如果设置了异步搜索,就不用过滤关键词 this.asyncSearchFlag 为 true
this.notSelectDataList = this.originList.filter(
ele =>
!selectListCheckId.includes(ele.id) &&
(ele.label.includes(this.noSelectKeyword) || this.asyncSearchFlag)
)
this.dataListNoCheck = this.originList.filter(
ele => !selectListCheckId.includes(ele.id)
)
},
// 提供获取已选数据的钩子
getSelectedData() {
return this.selectIdList
},
clearQueryInp(position) {
switch (position) {
case 'left':
this.$refs.noSelect.searchWord = ''
this.asyncSearchFlag && this.getDataByKeyword('')
break
case 'right':
this.$refs.hasSelect.searchWord = ''
break
default:
break
}
},
async getDataByKeyword(keyword, pageIndex) {
keyword = keyword.trim()
if (keyword) {
this.$nextTick(() => {
this.$refs.noSelect.asyncSearch = true
})
const resData = await this.getSearchData(
keyword,
pageIndex,
this.pageSize
)
if (Array.isArray(resData) && resData.length) {
this.asyncDataList = resData
this.notSelectDataList = resData
this.initData()
this.isLastPage = resData.length < this.pageSize
} else {
this.notSelectDataList = []
this.isLastPage = true
}
} else {
this.$refs.noSelect.asyncSearch = false
await this.getData(1)
}
},
async getData(pageIndex, changed = false) {
this.$nextTick(() => {
// 设置异步分页的 pageIndex
this.$refs.noSelect.asyncPageIndex = pageIndex
// 清空左侧输入框
this.$refs.noSelect.searchWord = ''
// asyncSearch 设置为 true
this.$refs.noSelect.asyncSearch = false
})
const resData = await this.getPageData(pageIndex, this.pageSize)
if (Array.isArray(resData) && resData.length) {
this.asyncDataList = resData
this.notSelectDataList = resData
// 这里必须是 true否则右侧不能搜索, 一搜索确认就不行了
this.initData(changed)
this.isLastPage = resData.length < this.pageSize
} else {
this.notSelectDataList = []
this.isLastPage = true
}
}
}
}
</script>
<style lang="scss" scoped>
.krry-main {
min-width: 600px;
}
.inner-center {
margin: 0 5px;
}
.buttons {
vertical-align: middle;
}
.opera {
position: relative;
display: inline-block;
vertical-align: middle;
text-align: center;
margin: 180px 8px;
width: 100%;
@media screen and (max-width: 992px) {
margin: 8px 8px;
text-align:start
}
.el-button.is-circle {
border-radius: 50%;
padding: 12px;
display: block;
margin: 25px auto;
}
.el-transfer__button {
padding: 5px;
}
}
.el-transfer-panel__filter .el-input__inner {
border-radius: 0;
}
</style>

View File

@@ -0,0 +1,397 @@
<template>
<div class="el-transfer-panel district-panel">
<div class="el-transfer-panel__header">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ title }}
</el-checkbox>
<span class="check-number">
{{ checkedData.length }}/{{ districtListMock.length }}
</span>
</div>
<div class="el-transfer-panel__body">
<div
v-if="filterable"
class="el-transfer-panel__filter el-input el-input--mini el-input--prefix"
>
<input
v-model.trim="searchWord"
:class="{ showClear: showClearBtn }"
:placeholder="filterPlaceholder"
autocomplete="off"
class="el-input__inner"
type="text"
@change="handleKeyword"
>
<span class="el-input__prefix" style="left: 0px">
<i class="el-input__icon el-icon-search" />
</span>
<span v-if="searchWord && showClearBtn" class="clear-input">
<i class="el-icon-circle-close" @click="clearInp" />
</span>
</div>
<el-checkbox-group
v-if="districtListMock.length > 0"
v-model="checkedData"
:class="{ expand: !filterable }"
@change="handleCheckedChange"
>
<el-checkbox
v-for="(item, index) in districtListMock"
:key="index"
:disabled="item.disabled"
:label="item"
:title="item.label"
class="el-transfer-panel__item"
>
<span v-html="isHighlight ? filterHighlight(item.label) : item.label" />
</el-checkbox>
</el-checkbox-group>
<p v-else class="no-data">{{ this.$t('common.NoData') }}</p>
</div>
<div class="vip-footer">
<el-button :disabled="disabledPre" class="v-page" plain small @click="prev">
{{ pageTexts[0] || defaultPrev }}
</el-button>
<el-button :disabled="disabledNex" class="v-page" plain small @click="next">
{{ pageTexts[1] || defaultNext }}
</el-button>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
title: {
type: String,
default: () => ''
},
operateId: {
type: Number,
default: () => 0
},
dataShowList: {
type: Array,
default: () => []
},
pageSize: {
type: Number,
default: () => 10
},
filterable: {
type: Boolean
},
filterPlaceholder: {
type: String,
default: () => 'Search'
},
pageTexts: {
type: Array,
default: () => ['', '']
},
async: {
type: Boolean,
default: () => false // 已选区不做异步
},
isLastPage: {
type: Boolean
},
isHighlight: {
type: Boolean
},
highlightColor: {
type: String,
default: () => '#409EFF'
},
asyncSearchFlag: {
// 是否设置了异步搜索方法
type: Boolean
},
showClearBtn: {
type: Boolean
}
},
data() {
return {
districtListMock: [], // 展示的数据 (搜索和分页会自动修改这个数组)
checkedData: [], // 已选择,数据格式:[id,id,id...]
isIndeterminate: false,
checkAll: false,
searchWord: '',
len: 0,
total: 0,
pageIndex: 0,
disabledPre: true,
disabledNex: false,
asyncSearch: false, // 要执行异步搜索的标记
asyncPageIndex: 1, // 异步分页的 pageIndex
asyncSearchPageIndex: 1, // 异步搜索的 pageIndex,
defaultPrev: '< ' + this.$tc('common.PagePrev'),
defaultNext: this.$tc('common.PageNext') + ' >'
}
},
watch: {
// 搜索框的监听器
searchWord(newWord) {
this.$emit('search-word', newWord, this.operateId)
},
// districtListMock 和 checkAll 的监听器
districtListMock() {
// 当方框中无已选择的数据时不能勾选checkBox
if (this.checkedData.length === 0) {
this.checkAll = false
this.isIndeterminate = false
}
},
checkedData(newWord) {
this.$emit('check-disable', newWord, this.operateId)
},
// 当列表中无数据时不能勾选checkBox
checkAll() {
this.checkAll = this.districtListMock.length === 0 ? false : this.checkAll
},
dataShowList: {
handler() {
this.async ? this.asyncInitData() : this.initData()
},
deep: true
}
},
created() {
this.initData()
},
methods: {
handleKeyword() {
this.asyncSearchPageIndex = 1
this.asyncSearchFlag &&
this.$emit(
'get-data-by-keyword',
this.searchWord,
this.asyncSearchPageIndex
)
},
// 分页数据
initData() {
this.len = this.dataShowList.length
this.total = Math.ceil(this.len / this.pageSize)
this.pageIndex = 0
this.pageData()
},
pageData() {
this.checkedData = []
if (this.total > 1 && this.pageIndex < this.total - 1) {
this.pageIndex === 0
? (this.disabledPre = true)
: (this.disabledPre = false)
this.disabledNex = false
this.districtListMock = this.dataShowList.slice(
this.pageIndex * this.pageSize,
this.pageIndex * this.pageSize + this.pageSize
)
} else {
this.total > 1 ? (this.disabledPre = false) : (this.disabledPre = true)
this.disabledNex = true
this.districtListMock = this.dataShowList.slice(
this.pageIndex * this.pageSize,
this.len
)
}
},
// 异步获取的数据,检查分页按钮可用性
asyncInitData() {
// 取消勾选
this.checkedData = []
// 分页按钮可用性
this.disabledNex = this.isLastPage
this.disabledPre =
this.asyncSearchFlag && this.asyncSearch
? this.asyncSearchPageIndex <= 1
: this.asyncPageIndex <= 1
// 赋值
this.districtListMock = this.dataShowList
},
// 上一页
prev() {
if (this.async) {
// 异步获取数据
this.disabledPre = true
this.asyncSearchFlag && this.asyncSearch
? this.$emit(
'get-data-by-keyword',
this.searchWord,
this.asyncSearchPageIndex <= 1 ? 1 : --this.asyncSearchPageIndex
)
: this.$emit(
'get-data',
this.asyncPageIndex <= 1 ? 1 : --this.asyncPageIndex
)
} else {
this.pageIndex > 0 && --this.pageIndex
this.pageData()
}
},
// 下一页
next() {
if (this.async) {
// 异步获取数据
this.disabledNex = true
this.asyncSearchFlag && this.asyncSearch
? this.$emit(
'get-data-by-keyword',
this.searchWord,
++this.asyncSearchPageIndex
)
: this.$emit('get-data', ++this.asyncPageIndex)
} else {
this.pageIndex <= this.total - 1 && ++this.pageIndex
this.pageData()
}
},
// 单选
handleCheckedChange(value) {
const checkedCount = value.length
this.checkAll = checkedCount === this.districtListMock.length
this.isIndeterminate =
checkedCount > 0 && checkedCount < this.districtListMock.length
// 子传父
this.$emit('check-district', value)
},
// 全选
handleCheckAllChange(val) {
this.checkedData = val ? this.districtListMock.filter(val => !val.disabled).map((val) => val) : []
this.isIndeterminate = false
// 子传父
this.$emit('check-district', this.checkedData)
},
clearInp() {
this.$emit('clear-input')
},
filterHighlight(label) {
const filterWord = this.searchWord.trim()
label = label && label.trim()
if (filterWord && label) {
const reg = new RegExp(filterWord)
return label.replace(reg, `<span style="color: ${this.highlightColor}">${filterWord}</span>`)
} else {
return label
}
}
}
}
</script>
<style lang="scss" scoped>
.district-panel {
width: 280px;
.el-transfer-panel__header {
.el-checkbox {
display: inline-block;
>>> .el-checkbox__label {
font-size: 14px;
}
}
}
.el-transfer-panel__body {
height: 335px;
//padding: 6px 0;
.el-transfer-panel__filter {
margin: 6px 14px;
line-height: 0;
.showClear {
padding-right: 30px;
border-radius: 0;
}
.clear-input {
position: absolute;
height: 100%;
right: 10px;
top: 0;
text-align: center;
color: #c0c4cc;
transition: all 0.3s;
line-height: 33px;
visibility: hidden;
opacity: 0;
&:hover {
color: #909399;
}
}
&:hover {
.clear-input {
opacity: 1;
visibility: visible;
}
}
}
}
.el-checkbox-group {
height: 295px;
overflow: auto;
&.expand {
height: 290px;
}
.el-transfer-panel__item {
display: block;
line-height: 28px;
height: 28px;
>>> .el-checkbox__label {
font-weight: 400;
line-height: 28px ;
}
>>> .el-checkbox__input {
top: 7px;
}
}
}
.check-number {
position: absolute;
right: 15px;
top: 0;
color: #909399;
font-size: 12px;
font-weight: 400;
}
.no-data {
font-size: 14px;
margin: 0;
height: 30px;
line-height: 30px;
padding: 6px 15px 0;
color: #909399;
text-align: center;
}
.vip-footer {
display: flex;
position: relative;
margin: 0;
text-align: center;
border-top: 1px solid #ebeef5;
.v-page {
width: 50%;
border: none;
margin: 0;
border-radius: 0;
padding: 10px 15px;
&:first-child {
border-right: 1px solid #ebeef5;
}
}
}
}
</style>

View File

@@ -52,7 +52,8 @@ export default {
},
data() {
return {
empty: () => {}
empty: () => {
}
}
},
computed: {

View File

@@ -1,5 +1,10 @@
<template>
<TagSearch :options="iOption" v-bind="$attrs" v-on="$listeners" />
<span>
<el-button v-if="shouldFold" circle class="search-btn" size="mini" @click="handleManualSearch">
<svg-icon icon-class="search" />
</el-button>
<TagSearch v-else :options="iOption" v-bind="$attrs" v-on="$listeners" @tag-search="handleTagSearch" />
</span>
</template>
<script>
@@ -25,17 +30,27 @@ export default {
exclude: {
type: Array,
default: () => []
},
// 建议折叠
fold: {
type: Boolean,
default: false
}
},
data() {
return {
internalOptions: []
internalOptions: [],
tags: [],
manualSearch: false
}
},
computed: {
iOption() {
const options = this.options.concat(this.internalOptions)
return _.uniqWith(options, _.isEqual)
},
shouldFold() {
return this.fold && this.tags.length === 0 && !this.manualSearch
}
},
watch: {
@@ -52,6 +67,19 @@ export default {
}
},
methods: {
handleTagSearch(tags) {
if (_.isEqual(tags, this.tags)) {
return
}
this.tags = tags
if (tags.length === 0) {
this.manualSearch = false
}
this.$emit('tagSearch', tags)
},
handleManualSearch() {
this.manualSearch = true
},
async genericOptions() {
const vm = this // 透传This
vm.internalOptions = [] // 重置
@@ -102,4 +130,11 @@ export default {
</script>
<style lang='less' scoped>
.search-btn {
margin-top: 4px;
cursor: pointer;
&:hover {
color: #409eff;
}
}
</style>

View File

@@ -28,6 +28,7 @@ import {
import i18n from '@/i18n/i18n'
import { newURL, replaceAllUUID } from '@/utils/common'
import ColumnSettingPopover from './components/ColumnSettingPopover.vue'
import LabelsFormatter from '@/components/Table/TableFormatters/LabelsFormatter.vue'
export default {
name: 'AutoDataTable',
@@ -140,6 +141,9 @@ export default {
case 'date_start':
col.formatter = DateFormatter
break
case 'labels':
col.formatter = LabelsFormatter
break
case 'comment':
col.showOverflowTooltip = true
}

View File

@@ -103,7 +103,9 @@ class StrategyPersistSelection extends StrategyAbstract {
*/
updateElTableSelection() {
const { data, id, selected } = this.elDataTable
data.forEach(r => {
// 历史勾选的行已经不在当前页了所以要将当前页的行数据和selected合并
const mergeData = _.uniqWith([...data, ...selected], _.isEqual)
mergeData.forEach(r => {
const isSelected = !!selected.find(r2 => r[id] === r2[id])
if (!this.elTable) {
return

View File

@@ -5,7 +5,7 @@
:destroy-on-close="true"
:title="$tc('common.Export')"
:visible.sync="exportDialogShow"
width="70%"
width="700px"
@cancel="handleExportCancel()"
@confirm="handleExportConfirm()"
>
@@ -21,7 +21,9 @@
:disabled="!option.can"
:label="option.value"
style="padding: 10px 20px;"
>{{ option.label }}</el-radio>
>
{{ option.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$tc('common.imExport.ExportRange')" :label-width="'100px'" class="export-form">

View File

@@ -8,7 +8,7 @@
:title="importTitle"
:visible.sync="showImportDialog"
class="importDialog"
width="70%"
width="900px"
@close="handleImportCancel"
>
<el-form v-if="!showTable" label-position="left" style="padding-left: 20px">
@@ -199,7 +199,8 @@ export default {
},
async getDownloadTemplateUrl(tp) {
const template = this.importOption === 'create' ? 'import' : 'update'
let query = `format=${tp}&template=${template}`
const action = this.importOption === 'create' ? 'create' : 'partial_update'
let query = `format=${tp}&template=${template}&action=${action}`
if (this.importOption === 'update' && this.selectedRows.length > 0) {
const resources = []
for (const item of this.selectedRows) {

View File

@@ -0,0 +1,187 @@
<template>
<div class="label-search">
<el-button
v-if="!showLabelSearch"
class="label-button"
size="small"
@click="showSearchSelect"
>
<svg-icon icon-class="tag" />
<span>{{ $t('common.Label') }}</span>
</el-button>
<el-cascader
v-else
ref="labelCascader"
v-model="labelValue"
:options="labelOptions"
:placeholder="placeholder"
:props="labelProps"
class="label-cascader"
clearable
filterable
separator=": "
size="small"
@focus="handleCascaderFocus"
@visible-change="handleCascaderVisibleChange"
>
<template slot-scope="{ node, data }">
<span>{{ data.label }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length -1 }}) </span>
</template>
<i slot="prefix" class="el-input__icon el-icon-search" />
</el-cascader>
</div>
</template>
<script>
export default {
name: 'LabelSearch',
data() {
return {
showLabelSearch: false,
labelProps: {
multiple: true
},
labelOptions: [],
labelValue: [],
placeholder: this.$t('labels.SelectLabelFilter')
}
},
watch: {
labelValue(newValue) {
if (!newValue || newValue.length === 0) {
this.showLabelSearch = false
}
if (!newValue || newValue.length === 0) {
this.$emit('labelSearch', '')
return
}
const labelSearch = newValue.map(item => item.join(':')).join(',')
this.$emit('labelSearch', labelSearch)
},
showLabelSearch(newValue) {
this.$emit('showLabelSearch', newValue)
}
},
mounted() {
this.$eventBus.$on('labelSearch', label => {
if (!label) {
this.labelValue = []
this.showLabelSearch = true
return
}
const labels = label.split(',').map(item => item.split(':'))
const notExistLabels = labels.filter(item => {
return !this.labelValue.find(label => label[0] === item[0] && label[1] === item[1])
})
this.labelValue = [...this.labelValue, ...notExistLabels]
this.getLabelOptions()
setTimeout(() => {
this.showLabelSearch = true
}, 500)
})
},
destroyed() {
this.$eventBus.$off('labelSearch')
},
methods: {
handleCascaderFocus() {
this.setSearchFocus()
},
handleCascaderVisibleChange(visible) {
if (visible) {
setTimeout(() => {
this.$refs.labelCascader.updateStyle()
},)
return
} else {
const input = this.$refs.labelCascader.$el.getElementsByClassName('el-input--suffix')[0].querySelector('input')
input.style.height = '34px'
}
if (this.labelValue.length === 0) {
this.showLabelSearch = false
}
},
getLabelOptions() {
if (this.labelOptions.length > 0) {
return
}
const url = '/api/v1/labels/labels/'
this.$axios.get(url).then(data => {
const groupedLabelOptions = _.groupBy(data, 'name')
const labelOptions = []
for (const [key, labels] of Object.entries(groupedLabelOptions)) {
const all = { value: '*', label: this.$t('common.All') }
const children = _.sortBy(labels, 'value').map(label => ({
value: label.value,
label: label.value
}))
labelOptions.push({
value: key,
label: key,
children: [all, ...children]
})
}
this.labelOptions = _.sortBy(labelOptions, 'label')
})
},
setSearchFocus() {
setTimeout(() => {
this.$refs.labelCascader.$el.getElementsByClassName('el-cascader__search-input')[0].focus()
}, 100)
},
showSearchSelect() {
this.getLabelOptions()
this.showLabelSearch = true
setTimeout(() => {
this.$refs.labelCascader.toggleDropDownVisible(true)
this.setSearchFocus()
}, 200)
}
}
}
</script>
<style lang='scss' scoped>
.label-search {
margin-right: 10px;
}
.label-button {
padding: 10px 13px 10px 12px;
}
.label-select {
}
.label-cascader {
width: 300px;
>>> .el-input--suffix.el-input {
input {
height: 34px;
}
}
>>> .el-input__inner {
font-size: 13px;
}
>>> .el-cascader__search-input {
display: none;
margin: 0 0 2px 13px;
}
>>> .el-input.is-focus + .el-cascader__tags .el-cascader__search-input {
display: inline;
}
>>> .el-input.is-focus + .el-cascader__tags {
flex-wrap: wrap;
}
>>> .el-cascader__tags {
white-space: nowrap;
flex-wrap: nowrap;
overflow: hidden;
}
}
</style>

View File

@@ -104,7 +104,7 @@ export default {
title: this.$t('common.BatchUpdate'),
name: 'actionUpdateSelected',
has: this.hasBulkUpdate,
icon: 'fa fa-refresh',
fa: 'batch-update',
can: function({ selectedRows }) {
let canBulkUpdate = vm.canBulkUpdate
if (typeof canBulkUpdate === 'function') {

View File

@@ -18,8 +18,14 @@
v-on="$listeners"
/>
<div :class="searchClass" class="search">
<LabelSearch
v-if="hasLabelSearch"
@labelSearch="handleLabelSearch"
@showLabelSearch="handleLabelSearchShowChange"
/>
<AutoDataSearch
v-if="hasSearch"
:fold="foldSearch"
class="right-side-item action-search"
v-bind="iSearchTableConfig"
@tagSearch="handleTagSearch"
@@ -41,12 +47,14 @@ import RightSide from './RightSide.vue'
import AutoDataSearch from '@/components/Table/AutoDataSearch/index.vue'
import DatetimeRangePicker from '@/components/Form/FormFields/DatetimeRangePicker.vue'
import { getDaysAgo, getDaysFuture } from '@/utils/common'
import LabelSearch from '@/components/Table/ListTable/TableAction/LabelSearch.vue'
const defaultTrue = { type: Boolean, default: true }
const defaultFalse = { type: Boolean, default: false }
export default {
name: 'TableAction',
components: {
LabelSearch,
LeftSide,
RightSide,
AutoDataSearch,
@@ -57,6 +65,7 @@ export default {
hasSearch: defaultTrue,
hasRightActions: defaultTrue,
hasDatePicker: defaultFalse,
hasLabelSearch: defaultFalse,
datePicker: {
type: Object,
default: () => ({
@@ -89,7 +98,8 @@ export default {
},
data() {
return {
keyword: ''
keyword: '',
foldSearch: false
}
},
computed: {
@@ -118,12 +128,22 @@ export default {
},
handleDateChange(val) {
this.datePick(val)
},
handleLabelSearch(val) {
if (!val || val.length === 0) {
this.searchTable({ labels: '' })
return
}
this.searchTable({ labels: val })
},
handleLabelSearchShowChange(val) {
this.foldSearch = val
}
}
}
</script>
<style scoped>
<style lang='scss' scoped>
.table-header {
/*display: flex;*/
/*flex-direction: row;*/
@@ -189,6 +209,12 @@ export default {
.left-side {
float: left;
display: block;
&>>> .action-item.el-dropdown {
height: 33px;
&> .el-button {
height: 100%;
}
}
}
.right-side {
@@ -232,4 +258,5 @@ export default {
.filter-field.right-side-item.action-search {
height: 34px;
}
</style>

View File

@@ -60,7 +60,7 @@ export default {
extraQuery = {
...extraQuery,
date_from: getDaysAgo(7).toISOString(),
date_to: getDayEnd().toISOString()
date_to: this.$moment(getDayEnd()).add(1, 'day').toISOString()
}
this.headerActions.datePicker = Object.assign({
dateStart: extraQuery.date_from,
@@ -173,12 +173,14 @@ export default {
this.dataTable.getList()
},
search(attrs) {
this.$log.debug('ListTable: search table', attrs)
this.$emit('TagSearch', attrs)
return this.dataTable?.search(attrs, true)
this.$refs.dataTable?.$refs.dataTable?.search(attrs, true)
},
filter(attrs) {
this.$emit('TagFilter', attrs)
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
this.$log.debug('ListTable: found filter change', attrs)
this.search(attrs)
},
hasActionPerm(action) {
const permRequired = this.permissions[action]

View File

@@ -0,0 +1,62 @@
<template>
<el-popover
:title="title"
placement="left-start"
trigger="click"
@show="getAsyncItems"
>
<div class="detail-content">
<div v-for="account of accountData" :key="account.id" class="detail-item">
<span>{{ account.name }}({{ account.username }})</span>
</div>
</div>
<el-button slot="reference" size="mini" type="primary">{{ $t('common.View') }}</el-button>
</el-popover>
</template>
<script>
import BaseFormatter from './base.vue'
export default {
name: 'SwitchFormatter',
extends: BaseFormatter,
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
value: this.cellValue,
accountData: []
}
},
computed: {
title() {
return this.formatterArgs.title || this.col.label
}
},
methods: {
async getAsyncItems() {
const userId = this.$route.params.id
const url = `/api/v1/perms/users/${userId}/assets/${this.row.id}`
this.$axios.get(url).then(res => {
this.accountData = res?.permed_accounts || []
})
}
}
}
</script>
<style scoped>
.detail-content {
max-height: 150px;
overflow-y: auto;
}
.detail-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:hover {
background-color: #F5F7FA;
}
}
</style>

View File

@@ -24,7 +24,6 @@ const defaultUpdateCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}
const updateRoute = this.colActions.updateRoute
console.log('Update route: ', updateRoute)
if (typeof updateRoute === 'object') {
route = Object.assign(route, updateRoute)

View File

@@ -3,17 +3,19 @@
<template>
<el-popover
:disabled="!showItems"
:open-delay="500"
:title="title"
placement="top-start"
trigger="hover"
width="400"
@show="getAsyncItems"
>
<div class="detail-content">
<div v-for="item of items" :key="getKey(item)" class="detail-item">
<span class="detail-item-name">{{ item }}</span>
</div>
</div>
<span slot="reference">{{ items && items.length }}</span>
<span slot="reference">{{ amount }}</span>
</el-popover>
</template>
</DetailFormatter>
@@ -37,52 +39,111 @@ export default {
showItems: true,
getItem(item) {
return item.name
}
},
async: false,
ajax: {},
title: ''
}
}
}
},
data() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs || {})
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs || {})
formatterArgs: formatterArgs,
data: formatterArgs.async ? [] : (this.cellValue || []),
amount: '',
asyncGetDone: false
}
},
computed: {
title() {
return this.formatterArgs.title || ''
return this.formatterArgs.title || this.col.label.replace('amount', '').replace('数量', '')
},
cellValueToRemove() {
return this.formatterArgs.cellValueToRemove || []
},
items() {
if (this.formatterArgs.async && !this.asyncGetDone) {
return [this.$t('common.tree.Loading') + '...']
}
const getItem = this.formatterArgs.getItem || (item => item.name)
let data = this.cellValue?.map(item => getItem(item)) || []
let data = []
if (Array.isArray(this.data)) {
data = this.data.map(item => getItem(item)) || []
} else {
// object {key: [value]}
data = Object.entries(this.data).map(([key, value]) => {
const item = { key: key, value: value }
return getItem(item)
}) || []
}
data = data.filter(Boolean)
return data
},
showItems() {
return this.formatterArgs.showItems !== false && this.cellValue?.length > 0
return this.amount !== 0 && this.amount !== ''
}
},
async mounted() {
if (this.formatterArgs.async) {
this.amount = this.cellValue
} else {
let cellValue = []
if (Array.isArray(this.cellValue)) {
cellValue = this.cellValue
} else {
// object {key: [value]}
cellValue = Object.keys(this.cellValue)
}
this.amount = (cellValue?.filter(value => !this.cellValueToRemove.includes(value)) || []).length
}
},
methods: {
getKey(item) {
const id = Math.random().toString(36).substring(2)
const id = Math.random().toString(36).substring(16)
return id + item
},
getDefaultUrl() {
const url = new URL(this.url, location.origin)
url.pathname += this.row.id + '/'
return url.pathname
},
async getAsyncItems() {
if (!this.formatterArgs.async) {
return
}
if (this.asyncGetDone) {
return
}
const url = this.formatterArgs.ajax.url || this.getDefaultUrl()
const params = this.formatterArgs.ajax.params || {}
const transform = this.formatterArgs.ajax.transform || (resp => resp[this.col.prop.replace('_amount', '')])
const response = await this.$axios.get(url, { params: params })
this.data = transform(response)
this.asyncGetDone = true
}
}
}
</script>
<style lang="scss" scoped>
.detail-content {
padding: 10px;
padding: 5px 10px;
max-height: 60vh;
overflow-y: auto;
}
.detail-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:hover {
background-color: #F5F7FA;
background-color: #F5F7FA;
}
}
.detail-item:first-child {
//border-top: 1px solid #EBEEF5;
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<el-button
ref="deleteButton"
:disabled="iDisabled"
size="mini"
type="danger"
:disabled="iDisabled"
@click="onDelete(col, row, cellValue, reload)"
>
<i class="fa fa-minus" />
@@ -16,9 +16,18 @@ import BaseFormatter from './base.vue'
export default {
name: 'DeleteActionFormatter',
extends: BaseFormatter,
data() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
return {
formatterArgs: formatterArgs
}
},
computed: {
iDisabled() {
// 禁用
if (this.formatterArgs.disabled !== undefined) {
return this.formatterArgs.disabled
}
return (this.disabled() || this.$store.getters.currentOrgIsRoot)
}
},

View File

@@ -5,6 +5,7 @@
:disabled="disabled"
:type="col.type || 'info'"
class="detail"
:class="{ 'clicked': linkClicked }"
@click="goDetail"
>
<slot>
@@ -30,6 +31,7 @@ export default {
routeQuery: null,
can: true,
openInNewPage: false,
removeColorOnClick: false,
getTitle({ col, row, cellValue }) {
return cellValue
},
@@ -43,6 +45,7 @@ export default {
data() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
return {
linkClicked: false,
formatterArgs: formatterArgs
}
},
@@ -100,6 +103,7 @@ export default {
methods: {
goDetail() {
if (this.formatterArgs.openInNewPage) {
this.linkClicked = this.formatterArgs.removeColorOnClick
const { href } = this.$router.resolve(this.detailRoute)
window.open(href, '_blank')
} else {
@@ -125,6 +129,11 @@ export default {
font-size: 13px;
}
.clicked,
.el-link.el-link--info.clicked {
color: inherit !important;
}
.icon {
width: 28px;
height: 28px;

View File

@@ -64,7 +64,7 @@ export default {
}
return text
}
return '-'
return this.items?.distribution || '-'
}
}
}

View File

@@ -0,0 +1,279 @@
<template>
<div class="label-container">
<a class="label-formatter-col">
<span v-if="!iLabels || iLabels.length === 0" style="vertical-align: top;">
<el-tag effect="plain" size="mini">
<i class="fa fa-tag" /> -
</el-tag>
</span>
<div v-else>
<div
v-for="label of iLabels"
:key="label"
>
<el-tag
:type="getLabelType(label)"
class="tag-formatter"
disable-transitions
effect="plain"
size="mini"
v-bind="formatterArgs.config"
@click="handleLabelSearch(label)"
>
<i class="fa fa-tag" /> <b> {{ getKey(label) }}</b>: {{ getValue(label) }}
</el-tag>
</div>
</div>
</a>
<a
v-if="formatterArgs.showEditBtn"
:class="[{ 'disabled-link': this.$store.getters.currentOrgIsRoot },'edit-btn']"
style="padding-left: 5px"
@click="showDialog = true"
>
<i class="fa fa-edit" />
</a>
<Dialog
v-if="showDialog"
:title="$tc('labels.BindLabel')"
:visible.sync="showDialog"
width="600px"
@cancel="handleCancel"
@confirm="handleConfirm"
>
<el-row :gutter="1" class="tag-select">
<el-col :span="12">
<Select2 v-model="keySelect2.value" v-bind="keySelect2" @change="handleKeyChanged" />
</el-col>
<el-col :span="12">
<Select2
v-model="valueSelect2.value"
:disabled="!keySelect2.value"
style="margin-left: 10px"
v-bind="valueSelect2"
@change="handleAddLabel"
/>
</el-col>
</el-row>
<div class="tag-zone">
<span v-if="!iLabels || iLabels.length === 0"> - </span>
<div v-else>
<el-tag
v-for="label of iLabels"
:key="label"
:type="getLabelType(label)"
class="tag-formatter"
closable
disable-transitions
effect="plain"
size="small"
v-bind="formatterArgs.config"
@close="handleCloseTag(label)"
>
<i class="fa fa-tag" /> <b>{{ getKey(label) }}</b>: {{ getValue(label) }}
</el-tag>
</div>
<div class="tag-tip">
<el-link @click="goToLabelList">
{{ $t('labels.LabelList') }} <i class="fa fa-external-link" />
</el-link>
</div>
</div>
</Dialog>
</div>
</template>
<script>
import BaseFormatter from './base.vue'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import Dialog from '@/components/Dialog'
export default {
name: 'LabelsFormatter',
components: { Select2, Dialog },
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
getLabelType(label) {
return 'primary'
},
getLabels(cellValue) {
return cellValue
},
config: {},
showEditBtn: true
}
}
}
},
data() {
return {
focusOn: '',
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
initial: [],
iLabels: [],
keySelect2: {
url: '/api/v1/labels/labels/keys/',
placeholder: this.$t('labels.SelectKeyOrCreateNew'),
// placeholder: this.$t('选择标签键或者创建新的'),
allowCreate: true,
value: '',
multiple: false
},
valueSelect2: {
url: '/api/v1/labels/labels/?name=blank',
placeholder: this.$t('labels.SelectValueOrCreateNew'),
// placeholder: '选择标签值或者创建新的',
allowCreate: true,
value: '',
multiple: false,
ajax: {
transformOption: (item) => {
return { label: item.value, value: item.value }
}
}
},
showDialog: false
}
},
computed: {
},
mounted() {
this.initial = this.formatterArgs.getLabels(this.cellValue)
this.iLabels = [...this.initial]
},
methods: {
handleLabelSearch(label) {
this.$eventBus.$emit('labelSearch', label)
},
getLabelType(tag) {
return this.formatterArgs.getLabelType(tag)
},
handleCloseTag(tag) {
this.iLabels = this.iLabels.filter(item => item !== tag)
},
handleKeyChanged(val) {
this.valueSelect2.url = `/api/v1/labels/labels/?name=${val}`
},
getKey(tag) {
return tag.split(':')[0]
},
getValue(tag) {
return tag.split(':').slice(1).join(':')
},
handleAddLabel() {
const key = this.keySelect2.value
const value = this.valueSelect2.value
if (!key || !value) {
return
}
const tag = `${key}:${value}`
if (this.iLabels.includes(tag)) {
return
}
this.iLabels.push(tag)
this.keySelect2.value = ''
this.valueSelect2.value = ''
this.$emit('input', this.iLabels)
},
handleCancel() {
this.showDialog = false
},
handleConfirm() {
const origin = _.sortBy(this.initial)
const current = _.sortBy(this.iLabels)
if (_.isEqual(origin, current)) {
return
}
const path = new URL(this.url, location.origin).pathname
const url = `${path}${this.row.id}/`
this.$axios.patch(url, { labels: this.iLabels }).then(res => {
this.$message.success('修改成功')
this.showDialog = false
})
},
goToLabelList() {
this.$router.push({ name: 'LabelList' })
}
}
}
</script>
<style lang="scss" scoped>
.tag {
display: flex;
flex-wrap: wrap;
& > span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.tag-select {
>>> .el-input__inner::placeholder {
font-size: 13px;
}
}
.edit-btn {
visibility: hidden;
position: relative;
transition: all 1s;
& > i {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}
.label-container {
display: flex;
.label-formatter-col {
overflow: hidden;
}
&:hover {
.edit-btn {
visibility: visible;
}
}
}
.tag-zone {
margin: 20px 0 0 0;
border: solid 1px #ebeef5;
padding: 10px;
background: #f2f2f5;
.tag-formatter {
margin: 1px 3px;
display: inline-block;
}
}
.input-button .el-button.el-button--mini {
padding: 5px;
height: 28px;
width: 28px;
margin-top: 3px;
}
.tag-formatter {
margin: 2px 0;
//display: table;
}
.tag-tip {
margin-top: 10px;
}
.disabled-link {
pointer-events: none;
color: grey;
cursor: default;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div v-if="display">
<el-switch v-model="value" @change="onChange" />
</div>
<span v-else>-</span>
</template>
<script>
import BaseFormatter from './base.vue'
export default {
name: 'SwitchFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
getPatchUrl(row) {
return ''
},
getPatchData(row) {
return {}
},
isDisplay(row) {
return true
}
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
value: this.cellValue
}
},
computed: {
patchUrl() {
return this.formatterArgs.getPatchUrl(this.row)
},
patchData() {
return this.formatterArgs.getPatchData(this.row)
},
display(row) {
return this.formatterArgs.isDisplay(this.row)
}
},
methods: {
onChange(val) {
this.$axios.patch(this.patchUrl, this.patchData).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.value = !val
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -4,9 +4,9 @@
v-for="tag of iTags"
:key="tag"
:type="getTagType(tag)"
v-bind="formatterArgs.config"
class="tag-formatter"
disable-transitions
v-bind="formatterArgs.config"
>
<i class="fa fa-tag" /> {{ tag }}
</el-tag>
@@ -15,6 +15,7 @@
<script>
import BaseFormatter from './base.vue'
export default {
name: 'TagsFormatter',
extends: BaseFormatter,
@@ -52,7 +53,7 @@ export default {
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
.tag {
display: flex;
flex-wrap: wrap;

View File

@@ -11,10 +11,13 @@ import DialogDetailFormatter from './DialogDetailFormatter.vue'
import EditableInputFormatter from './EditableInputFormatter.vue'
import StatusFormatter from './StatusFormatter.vue'
import TagsFormatter from './TagsFormatter.vue'
import LabelsFormatter from './LabelsFormatter.vue'
import ObjectRelatedFormatter from './ObjectRelatedFormatter.vue'
import TwoTabFormatter from './TwoTabFormatter.vue'
import ProtocolsFormatter from './ProtocolsFormatter.vue'
import TagChoicesFormatter from './TagChoicesFormatter.vue'
import SwitchFormatter from './SwitchFormatter.vue'
import AccountInfoFormatter from './AccountInfoFormatter.vue'
export default {
DetailFormatter,
@@ -33,7 +36,10 @@ export default {
ObjectRelatedFormatter,
TwoTabFormatter,
ProtocolsFormatter,
TagChoicesFormatter
TagChoicesFormatter,
LabelsFormatter,
SwitchFormatter,
AccountInfoFormatter
}
export {
@@ -53,5 +59,8 @@ export {
ObjectRelatedFormatter,
TwoTabFormatter,
ProtocolsFormatter,
TagChoicesFormatter
TagChoicesFormatter,
LabelsFormatter,
SwitchFormatter,
AccountInfoFormatter
}

View File

@@ -10,14 +10,14 @@
<el-tag
v-for="(v, k) in filterTags"
:key="k"
:disable-transitions="true"
:name="k"
class="filter-tag"
closable
size="small"
class="filter-tag"
type="info"
:disable-transitions="true"
@close="handleTagClose(k)"
@click="handleTagClick(v,k)"
@close="handleTagClose(k)"
>
<strong v-if="v.label">{{ v.label + ':' }}</strong>
<span v-if="v.valueLabel">{{ v.valueLabel }}</span>
@@ -27,14 +27,14 @@
<el-input
ref="SearchInput"
v-model="filterValue"
:placeholder="placeholder"
class="search-input"
:class="options.length < 1 ? 'search-input2': ''"
:placeholder="placeholder"
:validate-event="false"
class="search-input"
suffix-icon="el-icon-search"
@blur="focus = false"
@focus="focus = true"
@change="handleConfirm"
@focus="focus = true"
@keyup.enter.native="handleConfirm"
@keyup.delete.native="handleDelete"
/>
@@ -48,7 +48,8 @@ export default {
props: {
config: {
type: Object,
default: () => {}
default: () => {
}
},
options: {
type: Array,
@@ -56,7 +57,7 @@ export default {
},
getUrlQuery: {
type: Boolean,
default: () => true
default: () => false
},
default: {
type: Object,
@@ -122,6 +123,12 @@ export default {
},
deep: true
},
filterTags: {
handler() {
this.$emit('tag-search', this.filterMaps)
},
deep: true
},
filterValue(newValue, oldValue) {
if (newValue === '' && oldValue !== '') {
this.emptyCount = 1
@@ -206,15 +213,12 @@ export default {
delete routeFilter.search
}
const asFilterTags = _.cloneDeep(this.filterTags)
this.filterTags = {
...asFilterTags,
...routeFilter
}
if (Object.keys(this.filterTags).length > 0) {
setTimeout(() => {
return this.$emit('tagSearch', this.filterMaps)
}, 400)
}
setTimeout(() => {
this.filterTags = {
...asFilterTags,
...routeFilter
}
}, 100)
},
getValueLabel(key, value) {
for (const field of this.options) {
@@ -252,7 +256,7 @@ export default {
if (this.getUrlQuery) {
this.checkUrlFields(evt)
}
this.$emit('tagSearch', this.filterMaps)
// this.$emit('tagSearch', this.filterMaps)
return true
},
handleDelete() {
@@ -284,7 +288,7 @@ export default {
valueLabel: this.valueLabel
}
this.$set(this.filterTags, this.filterKey, tag)
this.$emit('tagSearch', this.filterMaps)
// this.$emit('tagSearch', this.filterMaps)
// 修改查询参数时改变url中保存的参数
if (this.getUrlQuery) {
@@ -342,70 +346,77 @@ export default {
</script>
<style lang="scss" scoped>
.filter-field {
display: flex;
align-items: center;
min-width: 198px;
border: 1px solid #dcdee2;
border-radius: 3px;
background-color:#fff;
.filter-field {
display: flex;
align-items: center;
min-width: 198px;
border: 1px solid #dcdee2;
border-radius: 3px;
background-color: #fff;
}
.search-input >>> .el-input__suffix {
cursor: pointer;
}
.search-input2 >>> .el-input__inner {
text-indent: 5px;
}
.search-input >>> .el-input__inner {
/*max-width:inherit !important;*/
}
max-width: 200px;
border: none;
padding-left: 5px;
}
.el-input >>> .el-input__inner{
border: none !important;
font-size: 13px;
}
.search-input > > > .el-input__suffix {
cursor: pointer;
}
.filterTitle {
padding-right: 2px;
line-height: 100%;
text-align: center;
flex-shrink: 0;
border-collapse: separate;
box-sizing: border-box;
color: rgb(96, 98, 102);
display: inline;
font-size: 13px;
height: auto;
}
.filter-tag{
margin: 2px 4px 2px 0;
}
.el-icon--right{
margin-left: 5px;
margin-right: 5px;
}
a {
color: #000;
}
.search-input2 > > > .el-input__inner {
text-indent: 5px;
}
.filter-field >>> .el-cascader .el-input--suffix .el-input__inner {
padding-right: 20px;
}
.search-input > > > .el-input__inner {
/*max-width:inherit !important;*/
.filter-field >>> .el-cascader .el-input input {
width: 0;
border: none;
}
max-width: 200px;
border: none;
padding-left: 5px;
}
.filter-field >>> .el-input__inner {
height: 30px;
}
.el-input > > > .el-input__inner {
border: none !important;
font-size: 13px;
}
.el-cascader-menu__wrap {
height: inherit;
}
.filterTitle {
padding-right: 2px;
line-height: 100%;
text-align: center;
flex-shrink: 0;
border-collapse: separate;
box-sizing: border-box;
color: rgb(96, 98, 102);
display: inline;
font-size: 13px;
height: auto;
}
.filter-tag {
margin: 2px 4px 2px 0;
}
.el-icon--right {
margin-left: 5px;
margin-right: 5px;
}
a {
color: #000;
}
.filter-field > > > .el-cascader .el-input--suffix .el-input__inner {
padding-right: 20px;
}
.filter-field > > > .el-cascader .el-input input {
width: 0;
border: none;
}
.filter-field > > > .el-input__inner {
height: 30px;
}
.el-cascader-menu__wrap {
height: inherit;
}
</style>

View File

@@ -86,6 +86,10 @@ export default {
treeTabConfig: {
type: Object,
default: () => ({})
},
treeWidth: {
type: String,
default: () => '20%'
}
},
data() {
@@ -142,6 +146,9 @@ export default {
},
selectNode: function(node) {
return this.$refs.AutoDataZTree.selectNode(node)
},
reloadTable() {
this.$refs.ListTable.reloadTable()
}
}
}

View File

@@ -91,7 +91,7 @@ export default {
let treeUrl
this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 &&
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
@@ -162,11 +162,15 @@ export default {
</span>`
if (rootNode) {
const $rootNodeRef = $('#' + rootNode.tId + '_a')
$rootNodeRef.css({ 'width': 'calc(100% - 68px)', 'overflow': 'hidden', 'text-overflow': 'ellipsis' })
$rootNodeRef.after(icons)
}
},
async refresh() {
this.treeSearchValue = ''
if (this.treeSetting?.callback?.beforeRefresh) {
this.treeSetting.callback.beforeRefresh()
}
if (this.treeSetting?.callback?.refresh) {
await this.treeSetting.callback.refresh()
}
@@ -387,6 +391,8 @@ div.rMenu li {
text-shadow: none;
top: 100%;
z-index: 1000;
height: 300px;
overflow: auto;
}
.ztree ::v-deep .fa {

View File

@@ -39,7 +39,7 @@ export default {
showRenameBtn: false,
drag: {
isCopy: false,
isMove: false
isMove: true
}
},
callback: {

View File

@@ -1,8 +1,8 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect' || index === levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ $tr( item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>

View File

@@ -8,7 +8,7 @@
</span>
</div>
</div>
<el-col :span="span">
<el-col :span="span" :style="{'height': height + 'px' }">
<el-input
v-model="iValue"
autosize
@@ -17,11 +17,11 @@
@change="onChange"
/>
</el-col>
<el-col v-if="isShow" :span="span">
<VueMarkdown class="result-html" :source="iValue" :show="true" :html="true" />
<el-col v-show="isShow" :span="span">
<VueMarkdown class="result-html" :source="iValue" :show="true" :html="false" />
</el-col>
</el-row>
<VueMarkdown v-else class="source" :source="iValue" :html="true" />
<VueMarkdown v-else class="source" :source="iValue" :html="false" />
</div>
</template>
@@ -49,11 +49,35 @@ export default {
},
data() {
return {
height: 0,
resizeObserver: null,
span: 12,
isShow: true,
iValue: this.value
}
},
mounted() {
this.$nextTick(() => {
this.resizeObserver = new ResizeObserver(entries => {
// 监听高度变化
const height = entries[0].target.offsetHeight
if (height) {
this.height = height
}
})
const el = document.querySelector('.result-html')
if (el) {
this.resizeObserver.observe(el)
}
})
},
beforeDestroy() {
const el = document.querySelector('.result-html')
if (el) {
this.resizeObserver.unobserve(el)
}
this.resizeObserver = null
},
methods: {
onChange() {
this.$emit('change', this.iValue)
@@ -76,15 +100,17 @@ export default {
font-size: 13px;
}
>>> .el-textarea__inner {
min-height: 210px !important;
>>> .el-textarea {
height: 100% !important;
.el-textarea__inner {
min-height: 210px !important;
height: 100% !important;
}
}
.source {
padding: 6px;
background-color: #f3f3f3;
}
.result-html {
height: 100%;
min-height: 210px;
margin-left: 4px;
padding: 5px 10px;

View File

@@ -12,7 +12,7 @@
type="default"
@click="item.callback()"
>
<i :class="item.icon" />
<svg-icon :icon-class="item.icon" />
</el-button>
</el-tooltip>
</div>
@@ -25,6 +25,7 @@
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { downloadText } from '@/utils/common'
export default {
name: 'Term',
@@ -37,7 +38,8 @@ export default {
},
xtermConfig: {
type: Object,
default: () => {}
default: () => {
}
}
},
data() {
@@ -56,24 +58,34 @@ export default {
toolbar: [
{
tip: this.$tc('ops.ScrollToTop'),
icon: 'fa fa-arrow-up',
icon: 'arrow-up',
callback: () => {
this.xterm.scrollToTop()
}
},
{
tip: this.$tc('ops.ScrollToBottom'),
icon: 'fa fa-arrow-down',
icon: 'arrow-down',
callback: () => {
this.xterm.scrollToBottom()
}
},
{
tip: this.$tc('ops.ClearScreen'),
icon: 'fa fa-refresh',
icon: 'refresh',
callback: () => {
this.xterm.reset()
}
},
{
tip: this.$tc('common.Export'),
icon: 'download',
callback: () => {
this.xterm.selectAll()
const text = this.xterm.getSelection()
const filename = `${this.$route.query?.type}_${this.$route.query?.taskId}.log`
downloadText(text, filename)
}
}
]
}

View File

@@ -16,6 +16,7 @@ export const attrMatchOptions = [
{ label: i18n.t('common.NotEqual'), value: 'not' },
{ label: i18n.t('common.MatchIn'), value: 'in' },
{ label: i18n.t('common.Contains'), value: 'contains' },
{ label: i18n.t('common.Exclude'), value: 'exclude' },
{ label: i18n.t('common.Startswith'), value: 'startswith' },
{ label: i18n.t('common.Endswith'), value: 'endswith' },
{ label: i18n.t('common.Regex'), value: 'regex' },

View File

@@ -27,6 +27,20 @@ export default {
hour: 'numeric', minute: 'numeric', hour12: true
}
},
'zh_hant': {
short: {
year: 'numeric', month: 'short', day: 'numeric'
},
medium: {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hourCycle: 'h23', hour12: false
},
long: {
year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric', hour12: true
}
},
'ja': {
short: {
year: 'numeric', month: 'short', day: 'numeric'

View File

@@ -10,7 +10,12 @@ Vue.use(VueI18n)
const cookieLang = VueCookie.get('django_language')
const browserLang = navigator.systemLanguage || navigator.language
let lang = cookieLang || browserLang || 'zh'
lang = lang.slice(0, 2)
if (lang === 'zh-hant') {
lang = 'zh_hant'
} else {
lang = lang.slice(0, 2)
}
const i18n = new VueI18n({
locale: lang,
fallbackLocale: 'en',

View File

@@ -1,6 +1,8 @@
{
"": "",
"accounts": {
"AccountName": "Account name",
"AccountBatchUpdate": "Batch update(same type)",
"SuFrom": "Su from",
"GenerateSuccessMsg": "Accounts generated successfully",
"GenerateAccounts": "Regenerate accounts",
@@ -31,6 +33,8 @@
"AccountGatherList": "Gather task"
},
"AccountChangeSecret": {
"OldSecret": "Old secret",
"NewSecret": "New secret",
"Result": "Result",
"ParamsHelpText": "The change secret parameter settings are currently only effective for assets with a platform type of host.",
"ExecutionTimes": "Execution times",
@@ -77,6 +81,7 @@
"ExecutionDetail": "Execution detail",
"Name": "Name",
"Retry": "Retry",
"BatchRetry": "Batch retry",
"Timer": "Timed execution",
"Detail": "Detail",
"TimeDelta": "Time delta",
@@ -96,7 +101,7 @@
"AccountPushCreate": "Account push create",
"AccountPushUpdate": "Account push update",
"ParamsHelpText": "The push parameter settings are currently only effective for assets with a platform type of host.",
"AccountPushList": "Account push list"
"AccountPushList": "Account push"
},
"AccountBackup": {
"IsSuccess": "Is success",
@@ -109,8 +114,8 @@
"ExecutionList": "Execution list",
"Reason": "Reason",
"AccountBackup": "Account backup",
"RecipientHelpText": "If both recipients A and B are set, the account key will be split into two parts: front and back",
"RecipientServer":"Receiving server"
"RecipientHelpText": "If both recipients A and B are set, the account key will be split into two parts: front and back. If the user has not set an encryption password, please go to Personal Information ->Preferences ->Set Encryption Password",
"RecipientServer": "Receiving server"
},
"DynamicUsername": "Dynamic username",
"AutoCreate": "Auto create",
@@ -118,11 +123,17 @@
"TaskID": "Task ID",
"AccountTemplate": "Account template",
"Sync": "Sync",
"SyncDelete": "Sync delete",
"BulkSyncDelete": "Bulk sync delete",
"SyncUpdateAccountInfo": "Sync update account info",
"AddAccountResult": "Add account result",
"AutoPush": "Auto Push",
"GeneralAccounts": "General Accounts",
"VirtualAccounts": "Virtual Accounts"
"VirtualAccounts": "Virtual Accounts",
"AccountDeleteConfirmMsg": "Delete account, do you want to continue?",
"Test": "Test",
"QuickTest": "Quick Test",
"BulkVerify": "Bulk Verify Connectivity"
},
"acl": {
"CommandFilterACLHelpMsg": "You can control whether commands can be executed on assets. Based on the rules, certain commands can be allowed while others are prohibited.",
@@ -252,6 +263,7 @@
"SecretType": "Secret type",
"PrivilegedTemplate": "Privileged",
"InitialDeploy": "Initial deploy",
"OnlyInitialDeploy": "Initial Setup",
"Address": "Address",
"PrivateKey": "Private key",
"Secret": "Secret",
@@ -473,7 +485,11 @@
"WebUpdate": "Update asset - Web",
"DatabaseUpdate": "Update asset - Database",
"GPTCreate": "Create asset - GPT",
"AppletHostDomainHelpText": "These domains are in System Organization"
"AppletHostDomainHelpText": "These domains are in System Organization",
"OracleDBNameHelpText": "Tips: Enter the Oracle database's SID or Service Name",
"Remove": "Remove",
"AddGatewayInDomain": "Add gateway",
"AddAssetInDomain": "Add asset"
},
"audits": {
"ChangeField": "Change field",
@@ -499,7 +515,7 @@
"AddPassKey": "AddPassKey"
},
"common": {
"BelongAll": "Include all",
"BelongAll": "Contains all",
"WordSep": " ",
"Enterprise": "Enterprise",
"SyncTask": "Synchronization task",
@@ -510,6 +526,8 @@
"StrategyUpdate": "Update strategy",
"StrategyDetail": "Strategy detail",
"Rule": "Rule",
"RuleRelation": "Rule relation",
"RuleRelationHelpTips": "And: The action is performed only when all conditions are met; Or: If a condition is met, the action will be performed",
"RuleCount": "Number of conditions",
"ActionCount": "Number of actions",
"PolicyName": "Policy name",
@@ -549,6 +567,7 @@
"Automations": "Automation",
"Sync": "Sync",
"Deploy": "Deploy",
"Uninstall": "Uninstall",
"Detail": "Detail",
"Selector": "Selector",
"NoContent": "No content",
@@ -600,6 +619,7 @@
"TemplateAdd": "Template add",
"TemplateHelpText": "When selecting a template to add, it will automatically create an account that does not exist under the asset and push it",
"PleaseAgreeToTheTerms": "Please agree to the terms",
"BindSuccess": "Bind success",
"PushSelected": "Push selected",
"PushSelectedSystemUsersToAsset": "Push selected system users to asset",
"TestSelected": "Test selected",
@@ -701,6 +721,7 @@
"BatchActivate": "Batch activate",
"SyncSelected": "Sync selected",
"bulkDeploy": "Bulk deploy",
"BulkVerify": "Bulk verify",
"bulkDeleteErrorMsg": "Bulk delete failed: ",
"bulkDeleteSuccessMsg": "Bulk delete success",
"bulkRemoveErrorMsg": "Bulk remove failed: ",
@@ -734,6 +755,20 @@
"Failed": "Failed",
"Pending": "Pending",
"ClickCopy": "Click copy",
"SyncUser": "Sync User",
"Reconnect": "Reconnect",
"NewChat": "New Chat",
"Chat": "Chat",
"Prompt": "Prompt",
"InputMessage": "Input message...",
"CollapseSidebar": "Collapse the sidebar",
"crontabDiffError": "Please ensure that the interval for scheduled execution is no less than ten minutes!",
"introduction": {
"ConceptTitle": "🤔 Python interpreter",
"ConceptContent": "I want you to act like a Python interpreter. I will give you Python code, and you will execute it. Do not provide any explanations. Do not respond with anything except the output of the code. ",
"IdeaTitle": "🌱 Linux Terminal",
"IdeaContent": "I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. When I need to tell you something in English, I will do so by putting text inside curly brackets {like this}. ."
},
"imExport": {
"ExportAll": "Export all",
"ExportOnlyFiltered": "Export only filtered",
@@ -749,6 +784,8 @@
"hasImportErrorItemMsg": "There is an error item, click the x icon to view the details, and continue to import after editing",
"ImportFail": "Import failed"
},
"ChatHello": "Hello! What help can I offer you?",
"ConnectionDropped": "Connection dropped",
"isValid": "Is valid",
"nav": {
"TempPassword": "Temporary password",
@@ -888,6 +925,7 @@
"Regex": "Regex",
"AttrValue": "Attribute Value",
"Contains": "Contains",
"Exclude": "Exclude",
"Match": "Match",
"Spec": "Specific",
"All": "All",
@@ -905,14 +943,25 @@
"IsActive": "Is active",
"LessEqualThan": "Less than or equal to",
"OtherRules": "Other rules",
"BelongTo": "Belong to",
"BelongTo": "Contains any",
"GreatEqualThan": "Greater than or equal to",
"PasswordRule": "PasswordRule",
"Uppercase": "Uppercase",
"Length": "Length",
"Digit": "Digit",
"Lowercase": "Lowercase",
"SpecialSymbol": "SpecialSymbol"
"PagePrev": "Previous",
"Selection": "Selection",
"PageNext": "Next",
"Selected": "Selected",
"SpecialSymbol": "Special symbol",
"ExcludeSymbol": "Exclude symbol",
"VirtualApps": "Virtual apps",
"ImageName": "Image name",
"ID": "ID",
"Ports": "Ports",
"EnterMessage": "Please enter a question, press Enter to send",
"Label": "Label"
},
"dashboard": {
"ActiveAsset": "Asset active",
@@ -936,29 +985,30 @@
"LoginCount": "Login count",
"LoginOverview": "Sessions overview",
"LoginTo": "Login to",
"LoginUsers": "Active accounts",
"ActiveUsers": "Active users",
"Monthly": "Monthly",
"CurrentConnections": "Current connections",
"TodayFailedConnections": "Connections failed today",
"CurrentConnectionUsers": "Current connection users",
"TodayFailedConnections": "Number of failed sessions today",
"OnlineSessions": "Online sessions",
"OnlineUserDevices": "Online user devices",
"RealTimeData": "Real-time data",
"UserAssetActivity": "Account/Asset activity",
"UserData": "Account data",
"LoginUserToday": "Login account today",
"UserAssetActivity": "User/asset activity status",
"UserData": "User data",
"LoginUserToday": "Login accounts today",
"AssetData": "Asset data",
"LoginAssetToday": "Active assets today",
"WeekAdd": "New this week",
"ProportionOfAssetTypes": "Proportion of asset types",
"Proportion": "Proportion",
"LoginUserRanking": "Login account ranking",
"ActiveAssetRanking": "Login asset ranking",
"LoginUserRanking": "Session user ranking",
"ActiveAssetRanking": "Session asset ranking",
"AssetName": "Asset name",
"NumberOfVisits": "Number of visits",
"ranking": "Ranking",
"Today": "Today",
"Last7Days": "Last 7 days",
"Last30Days": "Last30 days",
"Last7Days": "Last 7d",
"Last30Days": "Last 30d",
"OnlineUsers": "Online accounts",
"ConnectUsers": "Connect accounts",
"Num": "Num",
@@ -976,13 +1026,14 @@
"BatchCommandNotExecuted": "Batch command not executed",
"ExecuteFailedCommand": "Execute failed command",
"SessionTrend": "Session trend",
"SessionConnectTrend": "Session connection trends",
"UserLoginTrend": "Account login trend",
"TimesWeekUnit": "times/week",
"TopAssetsOfWeek": "Top assets of week",
"TopUsersOfWeek": "Top user of week",
"User": "User",
"UserRatio": "User Ratio",
"UsersTotal": "Accounts total",
"UsersTotal": "User total",
"Weekly": "Weekly",
"TotalJobFailed": "Total job failed",
"TotalJobRunning": "Total job running",
@@ -1098,6 +1149,8 @@
"SaveAdhoc": "Save Adhoc",
"AdhocManage": "Adhoc manage",
"LastPublishedTime": "Last published",
"ExecuteCycle": "Execute cycle",
"ExpectedNextExecuteTime": "Expected next execute time",
"CloseConfirmMessage": "The file has changed, do you want to save it?",
"privilegeOnly": "Select only privileged accounts",
"UploadPlaybook": "Upload Playbook",
@@ -1149,7 +1202,19 @@
"Add": "Add",
"SuccessfulOperation": "Successful operation",
"Modify": "Modify",
"AdhocUpdate": "Update Adhoc"
"AdhocUpdate": "Update Adhoc",
"Transfer": "Transfer",
"UploadDir": "Upload Directory",
"RequiredUploadFile": "Please upload files",
"DuplicateFileExists": "Uploading files with the same name is not allowed. Please delete the existing file with the same name.",
"NoFiles": "No Files",
"uploadFileLthHelpText": "Only files smaller than {limit}MB can be uploaded",
"FileSizeExceedsLimit": "File size exceeds limit",
"runSucceed": "Task executed successfully",
"EnterUploadPath": "Enter the upload path",
"FileNameTooLong": "File name too long",
"StopJob": "Stop job",
"StopLogOutput": "Task Canceled: The current task (currentTaskId) has been manually stopped. Since the progress of each task varies, the following is the final execution result of the task. A failed execution indicates that the task has been successfully stopped."
},
"perms": {
"": "",
@@ -1268,14 +1333,14 @@
"AssetCreate": "Asset create",
"AssetDetail": "Asset detail",
"AssetList": "Assets",
"AssetPermission": "Asset Permissions",
"AssetPermission": "Asset permissions",
"AssetPermissionCreate": "Asset permissions create",
"AssetPermissionDetail": "Asset permissions detail",
"AssetPermissionUpdate": "Asset permissions update",
"AssetUpdate": "Asset update",
"Assets": "Assets",
"LogsAudit": "Logs audit",
"SessionsAudit": "Sessions audit",
"LogsAudit": "Log audit",
"SessionsAudit": "Session audit",
"SessionList": "Session list",
"BatchCommand": "Batch Command",
"BatchScript": "Batch Script",
@@ -1303,7 +1368,7 @@
"DatabaseAppPermissionDetail": "Databases permissions detail",
"DatabaseAppPermissionUpdate": "Databases permissions update",
"DatabaseAppUpdate": "Database app update",
"KubernetesApp": "Kubernetes Apps",
"KubernetesApp": "Kubernetes apps",
"KubernetesAppCreate": "Kubernetes app create",
"KubernetesAppDetail": "Kubernetes app detail",
"KubernetesAppPermission": "Kubernetes permissions",
@@ -1317,11 +1382,11 @@
"UserAclUpdate": "User acl update",
"UserAclLists": "User acl lists",
"UserAclDetail": "User acl detail",
"AssetAclList": "Asset Acl",
"CommandFilterAclList": "Command Acl",
"CommandFilterAclCreate": "Command Acl create",
"CommandFilterAclDetail": "Command Acl detail",
"CommandFilterAclUpdate": "command Acl update",
"AssetAclList": "Asset connect acl",
"CommandFilterAclList": "Command acl",
"CommandFilterAclCreate": "Command acl create",
"CommandFilterAclDetail": "Command acl detail",
"CommandFilterAclUpdate": "command acl update",
"CommandGroupList": "Command group",
"CommandGroupCreate": "Command group create",
"CommandGroupDetail": "Command group detail",
@@ -1333,7 +1398,7 @@
"DomainDetail": "Domain detail",
"DomainList": "Domains",
"DomainUpdate": "Domain update",
"FileManager": "File Manager",
"FileManager": "File manager",
"FtpLog": "FTP Logs",
"GatewayCreate": "Gateway create",
"GatewayUpdate": "Gateway update",
@@ -1342,11 +1407,11 @@
"LabelCreate": "Label create",
"LabelList": "Labels",
"LabelUpdate": "Label update",
"LoginLog": "Login Logs",
"LoginLog": "Login logs",
"MyApps": "My Apps",
"MyAssets": "My Assets",
"OperateLog": "Operation Logs",
"PasswordChangeLog": "Password Update Logs",
"MyAssets": "My assets",
"OperateLog": "Operation logs",
"PasswordChangeLog": "Password change logs",
"Perms": "Permissions",
"PersonalInformationImprovement": "Personal Information Improvement",
"PlatformCreate": "Platform create",
@@ -1394,10 +1459,11 @@
"UserGuide": "UserGuide",
"UserList": "Users",
"UserProfile": "User profile",
"UserSession": "User Session",
"UserUpdate": "User update",
"Users": "Users",
"WebFTP": "WebFTP",
"WebTerminal": "Web Terminal",
"WebTerminal": "Web terminal",
"Notifications": "Notifications",
"SiteMessageList": "Site message",
"UserLoginACL": "User Login ACL",
@@ -1426,11 +1492,11 @@
"TicketsDone": "Ticket Done",
"TemplateUpdate": "Update template",
"Session": "Session",
"Templates": "模版管理",
"Templates": "Templates",
"AssetUserList": "Asset user",
"UserLoginACLUpdate": "Update User Login ACL",
"JobCreate": "Create job",
"JobExecutionLog": "Job Execution Log",
"JobExecutionLog": "Job execution log",
"JobList": "Job list",
"HostList": "Host list",
"UserLoginACLCreate": "Create User Login ACL",
@@ -1466,7 +1532,11 @@
"CloudCreate": "Create Asset - Cloud Platform",
"DatabaseUpdate": "Update Asset - Database",
"CloudUpdate": "Update Asset - Cloud Platform",
"OnlineUserDevices": "OnlineUserDevices"
"OnlineUserDevices": "OnlineUserDevices",
"More": "More...",
"VirtualAppDetail": "Virtual App Detail",
"AppProviderDetail": "Provider Detail",
"BulkTransfer": "Bulk transfer"
},
"rbac": {
"Permissions": "Permissions",
@@ -1475,6 +1545,7 @@
"terminal": {
"OnlineSessionHelpMsg": "The current session cannot be offline because it is an online session of the current user. Currently, only users who have logged in through web mode are recorded.",
"Offline": "Offline",
"Active": "Active",
"OfflineSuccessMsg": "Offline success",
"BulkOffline": "Bulk offline",
"Marketplace": "Marketplace",
@@ -1483,6 +1554,7 @@
"UploadSucceed": "Upload succeed",
"UploadFailed": "Upload failed",
"Applets": "Remote apps",
"AppletHelpText": "During the upload process, if the application does not exist, the application is created; if it already exists, the application is updated.",
"AppletHostSelectHelpMessage": "When connecting assets, the selection of the applet host is random. If you want to assign a specific one, you can specify the label AppletHost:AppletHostName in the asset label",
"AppletHosts": "Remote hosts",
"uploadZipTips": "Please upload zip file",
@@ -1491,7 +1563,11 @@
"DatabasePort": "Database protocol port",
"BasePort": "listening port",
"Endpoint": "Endpoint",
"ConnectMethod": "Connect method"
"ConnectMethod": "Connect method",
"VirtualApp": "Virtual app",
"VirtualAppDetail": "Virtual app detail",
"AppProvider": "App provider",
"Containers": "Containers"
},
"sessions": {
"DownloadFTPFileTip": "The current action is not recorded in the file, or the file size exceeds the threshold (100 MB by default), or is not saved to the corresponding storage",
@@ -1543,7 +1619,8 @@
"remoteAddr": "Remote addr",
"replay": "replay",
"replaySession": "Replay session",
"replayStorage": "Object storage",
"replayStorage": "Replay storage",
"objectStorage": "Object storage",
"riskLevel": "Risk level",
"session": "Session",
"sshPort": "SSH port",
@@ -1559,6 +1636,7 @@
"test": "Test",
"type": "Type",
"user": "Use",
"is_locked": "Is Locked",
"riskLevels": {
"common": "common"
},
@@ -1581,19 +1659,20 @@
"setting": {
"BlockedIPS": "Blocked IPS",
"ViewBlockedIPSHelpText": "View the list of locked IPs",
"LockedIP": "Locked IPs {count}",
"Unblock": "Unblock",
"BulkUnblock": "BulkUnblock",
"AppOps": "Task center",
"Ticket": "Ticket",
"TaskList": "Task list",
"Announcement": "Announcement",
"Features": "Features enable",
"Features": "Features",
"PasswordRule": "Password rule",
"PasswordSecurity": "Password security",
"SessionSecurity": "Session security",
"AuthSecurity": "Auth security",
"MsgSubscribe": "Message subscribe",
"Message": "Message setting",
"Message": "Messages",
"ServerTime": "Server time",
"Custom": "Custom",
"CleanHelpText": "Regular cleanup tasks will be executed at 2 o'clock in the morning every day, and the cleaned data cannot be recovered",
@@ -1699,6 +1778,8 @@
"MailSend": "Mail send",
"LDAPServerInfo": "LDAP Server",
"LDAPUser": "LDAP User",
"ChatAI": "Chat ai",
"Example": "Example: {example}",
"InsecureCommandAlert": "Insecure command alert",
"helpText": {
"TempPassword": "For a while, there is a period of 300 seconds, failure immediately after use",
@@ -1766,6 +1847,7 @@
"securityPasswordSpecialChar": "Must contain special characters",
"securityPasswordUpperCase": "Must contain capital letters",
"securityServiceAccountRegistration": "Service account registration",
"Storage": "Storage",
"siteUrl": "Current SITE URL",
"technologyConsult": "Technology Consult",
"terminalAssetListPageSize": "List page size",
@@ -1792,6 +1874,7 @@
"weComTest": "Test",
"FeiShu": "FeiShu",
"feiShuTest": "Test",
"Slack": "Slack",
"setting": "Setting",
"OtherAuth": "Other Auth",
"OAuth2LogoTip": "Tip: Authentication service provider (recommended image size: 64px*64px)",
@@ -1821,7 +1904,9 @@
"BasicTools": "Basic tool",
"sync": "Sync",
"AccountStorage": "Account Storage",
"Passkey": "Passkey"
"Passkey": "Passkey",
"ReplayStorageCreateUpdateHelpMessage": "Note: Currently, SFTP storage only supports account backup and does not support video storage.",
"VirtualApp": "Virtual App"
},
"tickets": {
"BatchApproval": "Batch approval",
@@ -1916,6 +2001,7 @@
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
},
"users": {
"OrgsAndRoles": "Organizations and roles",
"LunaSettingUpdate": "Luna setting",
"KokoSettingUpdate": "Koko setting",
"UserSetting": "User setting",
@@ -1938,7 +2024,7 @@
"Account": "Account",
"Existing": "Existing",
"UserInformation": "User information",
"Authentication": "Account",
"Authentication": "Authentication",
"Comment": "Comment",
"ConfirmPassword": "Confirm password",
"DateExpired": "Date expired",
@@ -1948,6 +2034,8 @@
"setWeCom": "Set wecom login",
"setDingTalk": "Set dingtalk login",
"setFeiShu": "Set feishu login",
"setLark": "Set lark login",
"setSlack": "Set Slack login",
"DatePasswordUpdated": "Date password updated",
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
"Email": "Email",
@@ -1955,6 +2043,7 @@
"WeCom": "WeCom",
"DingTalk": "DingTalk",
"FeiShu": "FeiShu",
"Slack": "Slack",
"FingerPrint": "Fingerprint",
"FirstLogin": "First login",
"InviteUser": "Invite user",
@@ -2045,7 +2134,10 @@
"passwordExpired": "Password expired",
"passwordWillExpiredPrefixMsg": "The password will expire in ",
"passwordWillExpiredSuffixMsg": " days.Please change your password as soon as possible.",
"dateLastLogin": "Date last login"
"dateLastLogin": "Date last login",
"AddAllMembersWarningMsg": "Are you sure you want to add all members?",
"disallowSelfUpdateFields": "Not allowed to modify the current fields oneself",
"GlobalDisableMfaMsg": "Global enforcement has been enabled"
},
"notifications": {
"MessageType": "Message Type",
@@ -2164,8 +2256,10 @@
"QingyunPrivatecloud": "Qingyun Private Cloud",
"HuaweiPrivatecloud": "Huawei Private Cloud",
"OpenStack": "OpenStack",
"ZStack": "ZStack",
"GCP": "Google Cloud Platform",
"UCloud": "UCloud Platform",
"Volcengine": "Volcengine",
"FC": "Fusion Compute",
"AWS_China": "AWS(China)",
"AWS_Int": "AWS(International)",
@@ -2174,8 +2268,13 @@
"JDCloud": "JD Cloud",
"Azure": "Azure(China)",
"Azure_Int": "Azure(International)",
"SCP": "SCP",
"ApsaraStack": "Apsara Stack",
"HostnameStrategy": "Used to produce the asset hostname. For example, 1. Instance name (instanceDemo)2. Instance name and Partial IP (instanceDemo-250.1)",
"IsAlwaysUpdate": "Asset info is kept up-to-date",
"IsAlwaysUpdate": "Keep assets up to date",
"FullySynchronous": "Assets fully synchronized",
"ReleaseAssets": "Release assets",
"ReleaseAssetsHelpTips": "Whether to automatically delete assets synchronized through this task and released on the cloud at the end of the task",
"AccountCreate": "Create account",
"AccountList": "Account list",
"AccountUpdate": "Update account",
@@ -2185,7 +2284,9 @@
"Provider": "Provider",
"Validity": "Validity",
"SyncStrategy": "Synchronisation strategy",
"IsAlwaysUpdateHelpTips": "Whether the asset information, including Hostname, IP, Platform, and AdminUser, is updated synchronously each time a synchronization task is performed",
"IsAlwaysUpdateHelpTips": "Whether to synchronize asset information, including host name, IP address, system platform, network domain, and node information, each time a synchronization task is executed",
"FullySynchronousHelpTips": "Whether to continue synchronizing assets when the asset conditions do not meet the matching policy rules",
"StrategyHelpTips": "A unique asset attribute (such as platform) is determined based on the policy priority. If multiple asset attributes (such as nodes) can be configured, all policy actions are executed",
"SyncInstanceTaskCreate": "Create sync task",
"SyncInstanceTaskList": "Sync task list",
"SyncInstanceTaskDetail": "Sync task detail",
@@ -2313,7 +2414,8 @@
"officialWebsite": "Official website link",
"Template": {
"Template": "Template"
}
},
"Footer": "Footer"
},
"applets": {
"PublishStatus": "Publish status",
@@ -2324,5 +2426,16 @@
"AccessIP": "Access IP",
"ApiKeyWarning": "To reduce the risk of AccessKey exposure, Secret is provided only during creation and cannot be queried again later. Please keep it safe.",
"PasskeyAddDisableInfo": "Your authentication source is {source}, and Passkey addition is not supported."
},
"labels": {
"BindLabel": "Bind label",
"LabelList": "Label list",
"SelectValueOrCreateNew": "Select label value or create new",
"SelectKeyOrCreateNew": "Select label key or create new",
"SelectLabelFilter": "Select label to filter",
"ResourceType": "Resource type",
"Resources": "Resources",
"BindResource": "Bind resource",
"SelectResource": "Select resource"
}
}

View File

@@ -29,7 +29,8 @@ actions_display_mapper = {
}
langs_display_map = {
'en': '英文',
'ja': '日文'
'ja': '日文',
'zh_Hant': '繁体中文',
}
@@ -114,7 +115,7 @@ if __name__ == '__main__':
'action', type=str, choices=("diff", "apply"),
)
parser.add_argument(
'langs', type=str, choices=("en", "ja"), nargs='*'
'langs', type=str, choices=("en", "ja", "zh_Hant"), nargs='*'
)
args = parser.parse_args()
action = args.action

View File

@@ -1,7 +1,9 @@
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'
import zhTWLocale from 'element-ui/lib/locale/lang/zh-TW'
import enLocale from 'element-ui/lib/locale/lang/en'
import jaLocale from 'element-ui/lib/locale/lang/ja'
import zh from './zh.json'
import zhHant from './zh_Hant.json'
import en from './en.json'
import ja from './ja.json'
@@ -10,6 +12,10 @@ export default {
...zhLocale,
...zh
},
zh_hant: {
...zhTWLocale,
...zhHant
},
en: {
...enLocale,
...en

View File

@@ -1,6 +1,8 @@
{
"": "",
"accounts": {
"AccountName": "Account name",
"AccountBatchUpdate": "ロット更新(同じタイプです)",
"Accounts": "アカウント",
"SuFrom": "から",
"AccountTemplateUpdateSecretHelpText": "アカウントリストには、テンプレートで作成されたアカウントが表示されます。暗号文を更新すると、テンプレートで作成されたアカウントの暗号文が更新されます。",
@@ -31,6 +33,8 @@
"AccountGatherList": "収集タスク"
},
"AccountChangeSecret": {
"OldSecret": "古い秘密",
"NewSecret": "新しい秘密",
"Result": "結果",
"ParamsHelpText": "改密パラメータの設定は、プラットフォームの種類がホストである資産に対してのみ有効です。",
"ExecutionTimes": "実行時間",
@@ -77,6 +81,7 @@
"ExecutionDetail": "実行の詳細",
"Name": "名前",
"Retry": "リトライ",
"BatchRetry": "一括リトライ",
"Timer": "時限実行",
"Detail": "詳細",
"TimeDelta": "営業時間",
@@ -109,20 +114,26 @@
"ExecutionList": "実行リスト",
"Reason": "理由",
"AccountBackup": "アカウントのバックアップ",
"RecipientHelpText": "受信者A Bが設定されている場合、アカウントの鍵は前後2つに分割されます",
"RecipientServer":"受信サーバー"
"RecipientHelpText": "受信者A Bが設定されている場合、アカウントの鍵は前後2つに分割されます。ユーザーが暗号化パスワードを設定していない場合-個人情報->プリファレンス設定で暗号化パスワードを設定してください",
"RecipientServer": "受信サーバー"
},
"DynamicUsername": "動的ユーザー名",
"AutoCreate": "自動作成",
"AccountExportTips": "エクスポート情報には機密情報を含むアカウント暗号文が含まれており、エクスポートされたフォーマットは暗号化されたzipファイルです暗号化パスワードが設定されていない場合は、個人情報にファイル暗号化パスワードを設定してください。",
"TaskID": "タスク ID",
"AccountTemplate": "账号模",
"AccountTemplate": "账号模",
"Sync": "同期",
"SyncDelete": "同期削除",
"BulkSyncDelete": "一括同期削除",
"SyncUpdateAccountInfo": "アカウント情報の同期更新",
"AddAccountResult": "账号批量添加结果",
"AutoPush": "自動プッシュ",
"GeneralAccounts": "一般アカウント",
"VirtualAccounts": "仮想アカウント"
"VirtualAccounts": "仮想アカウント",
"AccountDeleteConfirmMsg": "アカウントを削除します,続行しますか?",
"Test": "テスト",
"QuickTest": "クイックテスト",
"BulkVerify": "一括接続性テスト"
},
"acl": {
"CommandFilterACLHelpMsg": "コマンドフィルタリングを使用すると、コマンドがアセット上で実行されるかどうかを制御できます。ルールに基づいて、特定のコマンドは許可され、他のコマンドは禁止されることがあります。",
@@ -455,6 +466,7 @@
"Token": "トークン",
"GatewayList": "ゲートウェイ一覧",
"InitialDeploy": "初期展開",
"OnlyInitialDeploy": "初期設定のみ",
"PrivateKey": "鍵",
"Category": "カテゴリー",
"SSHPort": "SSH ポート",
@@ -473,7 +485,11 @@
"WebUpdate": "資産の更新 - Web",
"DatabaseUpdate": "資産の更新 - データベース",
"GPTCreate": "資産の作成 - GPT",
"AppletHostDomainHelpText": "これらのドメインはシステム組織にあります"
"AppletHostDomainHelpText": "これらのドメインはシステム組織にあります",
"OracleDBNameHelpText": "Oracle データベースの SID またはサービス名を入力してください",
"Remove": "取り除く",
"AddGatewayInDomain": "ゲートウェイの追加",
"AddAssetInDomain": "アセットの追加"
},
"audits": {
"ChangeField": "フィールドを変更します",
@@ -499,7 +515,7 @@
"AddPassKey": "パスキー(通行鍵)を追加"
},
"common": {
"BelongAll": "すべて",
"BelongAll": "すべてが含まれています",
"Enterprise": "企業版",
"SyncTask": "同期任務です",
"New": "新筑",
@@ -509,6 +525,8 @@
"StrategyUpdate": "ポリシーを更新します",
"StrategyDetail": "戦略の詳細です",
"Rule": "ルール",
"RuleRelation": "条件関係です",
"RuleRelationHelpTips": "且つ: すべての条件が満たされて初めて動作が実行されます; もしくは: 動作が実行されるという条件が満たされます",
"RuleCount": "条件数です",
"ActionCount": "アクション数",
"PolicyName": "ポリシーの名前です",
@@ -599,6 +617,7 @@
"Auth": "認証",
"bind": "紐付け",
"unbind": "バインド解除",
"BindSuccess": "バインド成功",
"PushSelected": "選択したプッシュ",
"PushSelectedSystemUsersToAsset": "選択したシステムユーザーをアセットにプッシュ",
"TestSelected": "テストを選択した",
@@ -698,6 +717,7 @@
"SyncSuccessMsg": "同期に成功しました",
"SyncSelected": "選択した同期",
"bulkDeploy": "一括デプロイ",
"BulkVerify": "一括テスト",
"bulkSyncErrorMsg": "一括同期に失敗しました:",
"bulkDeleteErrorMsg": "一括削除に失敗しました:",
"bulkDeleteSuccessMsg": "一括削除に成功しました",
@@ -735,6 +755,20 @@
"InputNumber": "数字の種類をお願いします。。",
"Receivers": "受取人",
"ClickCopy": "クリックしてコピー",
"SyncUser": "ユーザーを同期する",
"Reconnect": "再接続",
"NewChat": "新しいチャット",
"Chat": "チャット",
"Prompt": "ヒント",
"InputMessage": "メッセージの入力...",
"CollapseSidebar": "サイドバーを閉じる",
"crontabDiffError": "定期実行の間隔が10分以上であることをご確認ください",
"introduction": {
"ConceptTitle": "🤔 Python インタプリタ",
"IdeaContent": "私はあなたに Linux ターミナルの役割を果たしてもらいたいです。私はコマンドを入力し、ターミナルが表示すべき内容を返してもらいます。あなたにはユニークなコードブロック内でのみターミナルの出力に応答してほしいです。説明は書かないでください。私が何かを英語で伝える必要がある場合、中括弧で囲んでテキストを入れます {コメントテキスト}。",
"IdeaTitle": "🌱 Linux ターミナル",
"ConceptContent": "私はあなたに Python インタプリタのように行動してほしいです。私はPythonコードを提供し、それを実行してほしいです。説明は何も提供しないでください。コードの出力以外での応答は不要です。"
},
"imExport": {
"ExportAll": "すべてをエクスポート",
"ExportOnlyFiltered": "検索結果のみエクスポート",
@@ -750,6 +784,8 @@
"dragUploadFileInfo": "ここにファイルをドラッグするか、ここをクリックしてアップロードしてください",
"hasImportErrorItemMsg": "インポート失敗項目があります。左側のxをクリックして失敗原因を確認し、表をクリックして編集した後、失敗項目のインポートを続けることができます"
},
"ChatHello": "こんにちは!私はあなたを助けることができますか?",
"ConnectionDropped": "接続が切断されました",
"fileType": "ファイルタイプ",
"isValid": "有効",
"nav": {
@@ -856,6 +892,7 @@
"failedConditions": "条件に達していない結果!"
},
"Deploy": "配備",
"Uninstall": "アンインストールする",
"Publish": "リリース",
"Icon": "アイコン",
"Automations": "オートメーション",
@@ -886,6 +923,7 @@
"Regex": "正規表現",
"AttrValue": "属性値",
"Contains": "含む",
"Exclude": "含まない",
"Match": "一致する",
"Spec": "指定する",
"All": "すべて",
@@ -903,14 +941,26 @@
"IsActive": "アクティブです",
"LessEqualThan": "以下または等しい",
"OtherRules": "他のルール",
"BelongTo": "所属する",
"BelongTo": "どれも含まれます",
"GreatEqualThan": "以上または等しい",
"PasswordRule": "パスワードのルール",
"Uppercase": "大文字",
"Length": "長さ",
"Digit": "数字",
"Lowercase": "小文字",
"SpecialSymbol": "特殊記号"
"SpecialSymbol": "特殊記号",
"PagePrev": "前のページ",
"WordSep": "",
"Selection": "選択可能",
"PageNext": "次のページ",
"Selected": "選択済み",
"ExcludeSymbol": "記号を除外する",
"VirtualApps": "仮想アプリ",
"ImageName": "イメージ名",
"ID": "ID",
"Ports": "ポート",
"EnterMessage": "質問を入力してください、Enter キーを押して送信",
"Label": "ラベル"
},
"dashboard": {
"TotalJobLog": "ジョブ実行総数",
@@ -937,22 +987,23 @@
"LoginCount": "ログイン回数",
"LoginOverview": "セッション統計",
"LoginTo": "ログインしました",
"LoginUsers": "アクティブなアカウント",
"ActiveUsers": "アクティブユーザー",
"Monthly": "月ごと",
"CurrentConnections": "現在の接続数",
"TodayFailedConnections": "今日の接続に失敗しました",
"CurrentConnectionUsers": "現在のセッションユーザーの数",
"TodayFailedConnections": "今日の失敗したセッションの数",
"OnlineSessions": "オンラインセッション",
"RealTimeData": "リアルタイムデータ",
"UserAssetActivity": "アカウント/資産のアクティブ化",
"UserData": "アカウントデータ",
"LoginUserToday": "今日のログインアカウント数",
"UserAssetActivity": "ユーザー/アセットのアクティビティステータス",
"UserData": "ユーザーデータ",
"LoginUserToday": "今日のログインユーザー数",
"AssetData": "資産データ",
"LoginAssetToday": "今日のアクティブ資産数",
"WeekAdd": "今週の追加",
"ProportionOfAssetTypes": "資産タイプの割合",
"Proportion": "占有率",
"LoginUserRanking": "ログインアカウントランキング",
"ActiveAssetRanking": "ログイン資産ランキング",
"LoginUserRanking": "セッションユーザーランキング",
"ActiveAssetRanking": "セッションアセットランキング",
"AssetName": "資産名",
"NumberOfVisits": "アクセス回数",
"ranking": "ランキング",
@@ -976,13 +1027,14 @@
"BatchCommandNotExecuted": "未実行コマンド",
"ExecuteFailedCommand": "失敗コマンドの実行",
"SessionTrend": "セッショントレンド",
"SessionConnectTrend": "セッション接続の傾向",
"UserLoginTrend": "アカウントログイントレンド",
"TimesWeekUnit": "回/週",
"TopAssetsOfWeek": "週間資産TOP10",
"TopUsersOfWeek": "週ユーザーTOP10",
"User": "ユーザー",
"UserRatio": "ユーザー比率統計",
"UsersTotal": "アカウント総数",
"UsersTotal": "総ユーザー数",
"Weekly": "週ごと"
},
"ops": {
@@ -1091,6 +1143,8 @@
"SaveAdhoc": "保存コマンド",
"AdhocManage": "コマンド管理",
"LastPublishedTime": "最終公開",
"ExecuteCycle": "実行サイクル",
"ExpectedNextExecuteTime": "次回実行予定時刻",
"CloseConfirmMessage": "ファイルが変更されました,保存しますか?",
"privilegeOnly": "特権アカウントのみを選択",
"UploadPlaybook": "アップロード Playbook",
@@ -1146,7 +1200,19 @@
"Add": "追加",
"SuccessfulOperation": "成功した操作",
"Modify": "改訂",
"AdhocUpdate": "更新コマンド"
"AdhocUpdate": "更新コマンド",
"Transfer": "ファイルを転送する",
"UploadDir": "アップロードディレクトリ",
"RequiredUploadFile": "ファイルをアップロードしてください!",
"DuplicateFileExists": "同名のファイルのアップロードは許可されていません。同名のファイルを削除してください。",
"NoFiles": "まだファイルがありません",
"uploadFileLthHelpText": "{limit}MB 未満のファイルのみアップロードできます",
"FileSizeExceedsLimit": "ファイルサイズが制限を超えています",
"runSucceed": "タスクが成功しました",
"EnterUploadPath": "アップロードパスを入力してください",
"FileNameTooLong": "ファイル名が長すぎます",
"StopJob": "ジョブを停止",
"StopLogOutput": "Task Canceled現在のタスクcurrentTaskIdは手動で停止されました。各タスクの進行状況が異なるため、以下はタスクの最終実行結果です。実行が失敗した場合は、タスクが正常に停止されました。"
},
"perms": {
"": "",
@@ -1402,6 +1468,7 @@
"UserGuide": "ユーザーウィザード",
"UserList": "ユーザーリスト",
"UserProfile": "個人情報",
"UserSession": "ユーザーセッション",
"UserUpdate": "ユーザーの更新",
"Users": "ユーザー管理",
"WebFTP": "ファイル管理",
@@ -1459,7 +1526,11 @@
"DeviceUpdate": "アセット更新 - ネットワークデバイス",
"CloudCreate": "アセット作成 - クラウドプラットフォーム",
"DatabaseUpdate": "アセット更新 - データベース",
"CloudUpdate": "アセット更新 - クラウドプラットフォーム"
"CloudUpdate": "アセット更新 - クラウドプラットフォーム",
"More": "さらに..",
"BulkTransfer": "bulk transfer",
"AppProviderDetail": "アプリ提供者の詳細",
"VirtualAppDetail": "仮想アプリの詳細"
},
"rbac": {
"Permissions": "権限",
@@ -1467,6 +1538,7 @@
},
"terminal": {
"Offline": "オフライン",
"Active": "アクティブ",
"OnlineSessionHelpMsg": "セッションが現在のユーザーのオンラインセッションであるため、現在のセッションをオフラインできません。現在はWebログイン済みのユーザーのみが記録されています。",
"OfflineSuccessMsg": "オフラインに成功しました",
"BulkOffline": "オフライン",
@@ -1477,6 +1549,7 @@
"UploadFailed": "アップロードに失敗しました",
"AppletHostSelectHelpMessage": "アセットを接続する際、アプリケーションのデプロイホストの選択はランダムです。特定のアセットに固定のデプロイホストを割り当てたい場合は、AppletHost: <ホスト名>」というラベルを指定してください。",
"Applets": "リモートアプリケーション",
"AppletHelpText": "アップロード プロセス中に、アプリケーションが存在しない場合はアプリケーションが作成され、すでに存在する場合はアプリケーションが更新されます。",
"AppletHosts": "アプリケーションパブリッシャ",
"uploadZipTips": "zip形式のファイルをアップロードしてください",
"HostDeployment": "パブリッシャーの導入",
@@ -1484,7 +1557,11 @@
"DatabasePort": "データベース プロトコル ポート",
"BasePort": "リスニング ポート",
"Endpoint": "エンドポイント",
"ConnectMethod": "接続方式"
"ConnectMethod": "接続方式",
"Containers": "コンテナ",
"VirtualApp": "仮想アプリ",
"AppProvider": "アプリ提供者",
"VirtualAppDetail": "仮想アプリの詳細"
},
"sessions": {
"DownloadFTPFileTip": "現在の動作がファイルに記録されていないか、ファイルサイズが閾値(デフォルトでは100M)を超えているか、対応するストレージに保存されていません。",
@@ -1536,7 +1613,8 @@
"remoteAddr": "リモートアドレス",
"replay": "リプレイ",
"replaySession": "再生セッション",
"replayStorage": "オブジェクトストレージ",
"replayStorage": "ビデオ保存",
"objectStorage": "オブジェクトストレージ",
"riskLevel": "リスクレベル",
"session": "会話",
"sshPort": "SSHポート",
@@ -1552,6 +1630,7 @@
"test": "テスト",
"type": "タイプ",
"user": "ユーザー",
"is_locked": "一時停止するかどうか",
"riskLevels": {
"common": "普通"
},
@@ -1574,6 +1653,7 @@
"setting": {
"BlockedIPS": "ロックされたIP",
"ViewBlockedIPSHelpText": "ロックされたIPリストの表示",
"LockedIP": "ロックされた IP {count} 個",
"Unblock": "ロック解除",
"BulkUnblock": "一括ロック解除",
"AppOps": "タスクセンター",
@@ -1704,6 +1784,8 @@
"MailSend": "メール送信",
"LDAPServerInfo": "LDAPサーバー",
"LDAPUser": "LDAPユーザー",
"ChatAI": "チャットAI",
"Example": "例: {example}",
"helpText": {
"TempPassword": "一時パスワードの有効期間は300秒で、使用後すぐに失効します",
"ApiKeyList": "Api keyを使用してリクエストヘッダに署名します。リクエストのヘッダごとに異なります。使用ドキュメントを参照してください",
@@ -1770,6 +1852,7 @@
"securityPasswordSpecialChar": "特殊文字を含む必要があります",
"securityPasswordUpperCase": "大文字を含む必要があります",
"securityServiceAccountRegistration": "端末登録",
"Storage": "設定を保存する",
"siteUrl": "現在のサイトURL",
"technologyConsult": "技術コンサルティング",
"terminalAssetListPageSize": "資産ページングのページ数あたり",
@@ -1795,6 +1878,7 @@
"dingTalkTest": "テスト",
"weComTest": "テスト",
"FeiShu": "本を飛ばす",
"Slack": "Slack",
"SMS": "SMS設定",
"feiShuTest": "テスト",
"setting": "設定",
@@ -1812,7 +1896,9 @@
"Applets": "リモート アプリケーション",
"sync": "同期",
"AccountStorage": "アカウントストレージ",
"Passkey": "パスキー"
"Passkey": "パスキー",
"ReplayStorageCreateUpdateHelpMessage": "注: 現在、SFTP ストレージはアカウントのバックアップのみをサポートしており、ビデオ ストレージはサポートしていません。",
"VirtualApp": "仮想アプリケーション"
},
"tickets": {
"BatchApproval": "大量承認です",
@@ -1906,6 +1992,7 @@
"UpdateNodeAssetHardwareInfo": "ノード資産ハードウェア情報の更新"
},
"users": {
"OrgsAndRoles": "そしきとやくわり",
"LunaSettingUpdate": "Luna 設定更新",
"KokoSettingUpdate": "Koko 設定更新",
"UserSetting": "個人設定",
@@ -1940,6 +2027,7 @@
"WeCom": "企業wechat",
"DingTalk": "ホッチキス",
"FeiShu": "本を飛ばす",
"Slack": "Slack",
"FingerPrint": "指紋",
"FirstLogin": "初回ログイン",
"OrgUser": "組織ユーザー",
@@ -1952,6 +2040,8 @@
"setWeCom": "企業のwechat認証を設定する",
"setDingTalk": "ホッチキス認証の設定",
"setFeiShu": "飛書認証を設定する",
"setLark": "Lark認証を設定する",
"setSlack": "Slack認証を設定します",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "アカウントをより安全にするために、マルチファクタ认证を有効にします。 <Br/> 有効にすると、次回のログイン時に多因子認証バインディングプロセスに入ります。また、 (個人情報-> クイック修正-> 多因子設定の変更) もできます。で直接紐付けます",
"MFAOfUserFirstLoginUserGuidePage": "あなたと会社の安全を守るために、あなたのアカウント、パスワード、鍵などの重要な機密情報を大切に保管してください (例: 複雑なパスワードを設定し、多因子認証を有効にする) <br> メールボックス、携帯電話番号、微信などの個人情報は、ユーザー認証やプラットフォーム内部のメッセージ通知としてのみ使用されています。",
@@ -2033,7 +2123,10 @@
"SetPublicKey": "SSH公開鍵の設定",
"passwordExpired": "パスワードが期限切れです",
"passwordWillExpiredPrefixMsg": "パスワードはまもなく",
"passwordWillExpiredSuffixMsg": "期限が切れた後、できるだけ早くパスワードを変更してください。"
"passwordWillExpiredSuffixMsg": "期限が切れた後、できるだけ早くパスワードを変更してください。",
"AddAllMembersWarningMsg": "すべてのメンバーを追加してもよろしいですか?",
"disallowSelfUpdateFields": "現在のフィールドを自分で変更することは許可されていません",
"GlobalDisableMfaMsg": "グローバルでの強制が有効になっています"
},
"notifications": {
"MessageType": "メッセージタイプ",
@@ -2156,9 +2249,11 @@
"QingyunPrivatecloud": "青雲プライベートクラウド",
"HuaweiPrivatecloud": "ファーウェイプライベートクラウド",
"OpenStack": "OpenStack",
"ZStack": "ZStack",
"CTYunPrivate": "天翼プライベート・クラウド",
"GCP": "Googleクラウド",
"UCloud": "UCloudユケデです",
"Volcengine": "Volcengine",
"FC": "Fusion Compute",
"LAN": "ローカルエリアネットワーク",
"AWS_China": "AWS(中国)",
@@ -2169,8 +2264,13 @@
"KingSoftCloud": "金山雲",
"Azure": "Azure(中国)",
"Azure_Int": "Azure (国際)",
"SCP": "SCP",
"ApsaraStack": "Apsara Stack",
"HostnameStrategy": "資産を生成するためにホスト名。例: 1. インスタンス名 (instanceDemo) 2.インスタンス名と一部IP (下位2桁) (instanceDemo-250.1)",
"IsAlwaysUpdate": "資産情報を最新の状態に保つ",
"IsAlwaysUpdate": "資産は常に最新です",
"FullySynchronous": "資産完全にシンクロします",
"ReleaseAssets": "資産の同期解放",
"ReleaseAssetsHelpTips": "タスクの終了時に、このタスクを介して同期され、クラウド上で解放された資産を自動的に削除するかどうか",
"AccountCreate": "アカウントの作成",
"AccountList": "アカウントリスト",
"AccountUpdate": "アカウントの更新",
@@ -2180,7 +2280,9 @@
"Provider": "クラウドサービス業者",
"Validity": "有効",
"SyncStrategy": "同調戦略です",
"IsAlwaysUpdateHelpTips": "同期タスク実行るたびに、ホスト名、IP、システムプラットフォーム、管理ユーザーなど、アセットの情報を同期更新しますか?",
"IsAlwaysUpdateHelpTips": "同期タスク実行されるたびに、アセットの情報が同期更新されるかどうかです。ホスト名、IP、システムプラットフォーム、ドメイン、ードなどの情報が含まれます",
"FullySynchronousHelpTips": "アセット条件が適合ポリシールールを満たさない場合、このようなアセットを同期させ続けるかどうかです",
"StrategyHelpTips": "プラットフォームのようなアセットの固有の属性がポリシーの優先順位に基づいて決定され、ノードのようなアセットの属性が複数構成されると、すべてのポリシーのアクションが実行されます",
"SyncInstanceTaskCreate": "同期タスクを作成します",
"SyncInstanceTaskList": "同期タスクリストです",
"SyncInstanceTaskDetail": "同期任務詳細です",
@@ -2303,7 +2405,8 @@
"CrontabOfCreateUpdatePage": "例: 毎週日曜日03:05に <5 3 * * 0> <br/> 5桁のLinux crontab式を使用して、時分割日月曜日> (<a href = \"https://tool.lu/crontab/\" target = \"_ ツール </a>) <br/> 定期実行とサイクル実行を同時に設定している場合は、優先的に定期実行を使って",
"IntervalOfCreateUpdatePage": "単位: 時",
"UsernameOfCreateUpdatePage": "ターゲットホスト上のユーザーのユーザー名存在する場合は、ユーザーパスワードを変更します存在しない場合は、ユーザーを追加してパスワードを設定します"
}
},
"Footer": "フッター"
},
"applets": {
"PublishStatus": "投稿ステータス",
@@ -2314,5 +2417,16 @@
"AccessIP": "Access IP",
"ApiKeyWarning": "AccessKeyの漏洩リスクを低減するため、Secretは作成時にのみ提供され、後で再度クエリできません。安全に保管してください。",
"PasskeyAddDisableInfo": "あなたの認証元は {source} であり、Passkeyの追加はサポートされていません。"
},
"labels": {
"BindLabel": "ラベルをバインド",
"LabelList": "ラベルリスト",
"SelectValueOrCreateNew": "ラベル値を選択するか新規作成",
"SelectKeyOrCreateNew": "ラベルキーを選択するか新規作成",
"SelectLabelFilter": "フィルタ用ラベルを選択",
"ResourceType": "リソースタイプ",
"Resources": "リソース",
"BindResource": "リソースを関連付ける",
"SelectResource": "リソースを選択"
}
}

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